Compare commits
16 Commits
docs/add-f
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92164309ab | ||
|
|
184050f97d | ||
|
|
7887a695f7 | ||
|
|
654a76c5db | ||
|
|
a339d8fc75 | ||
|
|
482789ca41 | ||
|
|
28a66fba92 | ||
|
|
84f364de74 | ||
|
|
39c955cdc4 | ||
|
|
e02ca54187 | ||
|
|
ac46ee004b | ||
|
|
17a6eb260d | ||
|
|
5ea80366be | ||
|
|
99aa0cb980 | ||
|
|
3de53a76d0 | ||
|
|
7fa9b6aff8 |
157
CONTRIBUTING.md
157
CONTRIBUTING.md
@@ -239,6 +239,163 @@ git push origin feature/my-feature
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
### Keeping Your Feature Branch Up-to-Date
|
||||
|
||||
**Best practice for teams:** Sync your feature branch with `develop` regularly to avoid large merge conflicts.
|
||||
|
||||
#### Daily Start-of-Work Routine
|
||||
|
||||
```bash
|
||||
# 1. Get latest changes from develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
# 2. Update your feature branch
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
|
||||
# 3. If there are conflicts (see below), resolve them
|
||||
# 4. Continue working
|
||||
```
|
||||
|
||||
**How often?**
|
||||
- Minimum: Once per day (start of work)
|
||||
- Better: Multiple times per day if develop is active
|
||||
- Always: Before creating your Pull Request
|
||||
|
||||
#### Merge vs Rebase: Which to Use?
|
||||
|
||||
**For teams of 5+ developers, use `merge` (not `rebase`):**
|
||||
|
||||
```bash
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
```
|
||||
|
||||
**Why merge over rebase?**
|
||||
- ✅ Safer: Preserves your commit history
|
||||
- ✅ Collaborative: Works when multiple people work on the same feature branch
|
||||
- ✅ Transparent: Shows when you integrated upstream changes
|
||||
- ✅ No force-push: Once you've pushed to origin, merge won't require `--force`
|
||||
|
||||
**When to use rebase:**
|
||||
- ⚠️ Only if you haven't pushed yet
|
||||
- ⚠️ Only if you're the sole developer on the branch
|
||||
- ⚠️ You want a cleaner, linear history
|
||||
|
||||
```bash
|
||||
# Only do this if you haven't pushed yet!
|
||||
git checkout feature/my-feature
|
||||
git rebase develop
|
||||
```
|
||||
|
||||
**Never rebase after pushing** - it rewrites history and breaks collaboration.
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
When you run `git merge develop` and see conflicts:
|
||||
|
||||
```bash
|
||||
$ git merge develop
|
||||
Auto-merging app/Http/Controllers/OrderController.php
|
||||
CONFLICT (content): Merge conflict in app/Http/Controllers/OrderController.php
|
||||
Automatic merge failed; fix conflicts and then commit the result.
|
||||
```
|
||||
|
||||
**Step-by-step resolution:**
|
||||
|
||||
1. **See which files have conflicts:**
|
||||
```bash
|
||||
git status
|
||||
# Look for "both modified:" files
|
||||
```
|
||||
|
||||
2. **Open conflicted files** - look for conflict markers:
|
||||
```php
|
||||
<<<<<<< HEAD
|
||||
// Your code
|
||||
=======
|
||||
// Code from develop
|
||||
>>>>>>> develop
|
||||
```
|
||||
|
||||
3. **Resolve conflicts** - edit the file to keep what you need:
|
||||
```php
|
||||
// Choose your code, their code, or combine both
|
||||
// Remove the <<<, ===, >>> markers
|
||||
```
|
||||
|
||||
4. **Mark as resolved:**
|
||||
```bash
|
||||
git add app/Http/Controllers/OrderController.php
|
||||
```
|
||||
|
||||
5. **Complete the merge:**
|
||||
```bash
|
||||
git commit -m "merge: resolve conflicts with develop"
|
||||
```
|
||||
|
||||
6. **Run tests to ensure nothing broke:**
|
||||
```bash
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
7. **Push the merge commit:**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
#### When Conflicts Are Too Complex
|
||||
|
||||
If conflicts are extensive or you're unsure:
|
||||
|
||||
1. **Abort the merge:**
|
||||
```bash
|
||||
git merge --abort
|
||||
```
|
||||
|
||||
2. **Ask for help** in #engineering Slack:
|
||||
- "I'm merging develop into feature/X and have conflicts in OrderController"
|
||||
- Someone might have context on the upstream changes
|
||||
|
||||
3. **Pair program the resolution** - screen share with the person who made the conflicting changes
|
||||
|
||||
4. **Alternative: Start fresh** (last resort):
|
||||
```bash
|
||||
# Create new branch from latest develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/my-feature-v2
|
||||
|
||||
# Cherry-pick your commits
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
#### Example: Multi-Day Feature Work
|
||||
|
||||
```bash
|
||||
# Monday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Get latest changes
|
||||
# Work all day, make commits
|
||||
|
||||
# Tuesday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Sync again (someone added auth changes)
|
||||
# Continue working
|
||||
|
||||
# Wednesday
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Final sync before PR
|
||||
git push origin feature/payment-integration
|
||||
# Create Pull Request
|
||||
```
|
||||
|
||||
**Result:** Small, manageable syncs instead of one huge conflict on PR day.
|
||||
|
||||
### When to Test Locally
|
||||
|
||||
**Always run tests before pushing if you:**
|
||||
|
||||
224
app/Http/Controllers/Seller/CategoryController.php
Normal file
224
app/Http/Controllers/Seller/CategoryController.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Seller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Business;
|
||||
use App\Models\ComponentCategory;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(Business $business)
|
||||
{
|
||||
// Load product categories with nesting and counts
|
||||
$productCategories = ProductCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => function ($query) {
|
||||
$query->orderBy('sort_order')->orderBy('name');
|
||||
}])
|
||||
->withCount('products')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Load component categories with nesting and counts
|
||||
$componentCategories = ComponentCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => function ($query) {
|
||||
$query->orderBy('sort_order')->orderBy('name');
|
||||
}])
|
||||
->withCount('components')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.index', compact('business', 'productCategories', 'componentCategories'));
|
||||
}
|
||||
|
||||
public function create(Business $business, string $type)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Get all categories of this type for parent selection
|
||||
$categories = $type === 'product'
|
||||
? ProductCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
: ComponentCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.create', compact('business', 'type', 'categories'));
|
||||
}
|
||||
|
||||
public function store(Request $request, Business $business, string $type)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$tableName = $type === 'product' ? 'product_categories' : 'component_categories';
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => "nullable|exists:{$tableName},id",
|
||||
'description' => 'nullable|string',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'image' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$validated['business_id'] = $business->id;
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
||||
|
||||
// Handle image upload
|
||||
if ($request->hasFile('image')) {
|
||||
$validated['image_path'] = $request->file('image')->store('categories', 'public');
|
||||
}
|
||||
|
||||
// Validate parent belongs to same business if provided
|
||||
if (! empty($validated['parent_id'])) {
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$parent = $model::where('business_id', $business->id)->find($validated['parent_id']);
|
||||
|
||||
if (! $parent) {
|
||||
return back()->withErrors(['parent_id' => 'Invalid parent category'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$model::create($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category created successfully');
|
||||
}
|
||||
|
||||
public function edit(Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
// Get all categories of this type for parent selection (excluding self and descendants)
|
||||
$categories = $model::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->where('id', '!=', $id)
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.edit', compact('business', 'type', 'category', 'categories'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
$tableName = $type === 'product' ? 'product_categories' : 'component_categories';
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => "nullable|exists:{$tableName},id",
|
||||
'description' => 'nullable|string',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'image' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
||||
|
||||
// Handle image upload
|
||||
if ($request->hasFile('image')) {
|
||||
// Delete old image if exists
|
||||
if ($category->image_path) {
|
||||
\Storage::disk('public')->delete($category->image_path);
|
||||
}
|
||||
$validated['image_path'] = $request->file('image')->store('categories', 'public');
|
||||
}
|
||||
|
||||
// Validate parent (can't be self or descendant)
|
||||
if (! empty($validated['parent_id'])) {
|
||||
if ($validated['parent_id'] == $id) {
|
||||
return back()->withErrors(['parent_id' => 'Category cannot be its own parent'])->withInput();
|
||||
}
|
||||
|
||||
$parent = $model::where('business_id', $business->id)->find($validated['parent_id']);
|
||||
if (! $parent) {
|
||||
return back()->withErrors(['parent_id' => 'Invalid parent category'])->withInput();
|
||||
}
|
||||
|
||||
// Check for circular reference (if parent's parent is this category)
|
||||
if ($parent->parent_id == $id) {
|
||||
return back()->withErrors(['parent_id' => 'This would create a circular reference'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$category->update($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category updated successfully');
|
||||
}
|
||||
|
||||
public function destroy(Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
// Check if has products/components
|
||||
if ($type === 'product') {
|
||||
$count = $category->products()->count();
|
||||
if ($count > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$count} products. Please reassign or delete products first.");
|
||||
}
|
||||
} else {
|
||||
$count = $category->components()->count();
|
||||
if ($count > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$count} components. Please reassign or delete components first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if has children
|
||||
$childCount = $category->children()->count();
|
||||
if ($childCount > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$childCount} subcategories. Please delete or move subcategories first.");
|
||||
}
|
||||
|
||||
// Delete image if exists
|
||||
if ($category->image_path) {
|
||||
\Storage::disk('public')->delete($category->image_path);
|
||||
}
|
||||
|
||||
$category->delete();
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category deleted successfully');
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line created successfully.');
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line updated successfully.');
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ProductLineController extends Controller
|
||||
$productLine->delete();
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line deleted successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,89 @@ use Illuminate\Http\Request;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the personal profile page.
|
||||
*/
|
||||
public function profile(Business $business)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// Get login history (assuming a login_histories table exists)
|
||||
$loginHistory = collect(); // Placeholder - will be implemented with login history tracking
|
||||
|
||||
return view('seller.settings.profile', compact('business', 'loginHistory'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the personal profile.
|
||||
*/
|
||||
public function updateProfile(Business $business, Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email,'.$user->id,
|
||||
'avatar' => 'nullable|image|max:2048',
|
||||
'remove_avatar' => 'nullable|boolean',
|
||||
'use_gravatar' => 'nullable|boolean',
|
||||
'linkedin_url' => 'nullable|url|max:255',
|
||||
'twitter_url' => 'nullable|url|max:255',
|
||||
'facebook_url' => 'nullable|url|max:255',
|
||||
'instagram_url' => 'nullable|url|max:255',
|
||||
'github_url' => 'nullable|url|max:255',
|
||||
]);
|
||||
|
||||
// Handle avatar removal
|
||||
if ($request->has('remove_avatar') && $user->avatar_path) {
|
||||
\Storage::disk('public')->delete($user->avatar_path);
|
||||
$validated['avatar_path'] = null;
|
||||
}
|
||||
|
||||
// Handle avatar upload
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Delete old avatar if exists
|
||||
if ($user->avatar_path) {
|
||||
\Storage::disk('public')->delete($user->avatar_path);
|
||||
}
|
||||
|
||||
$path = $request->file('avatar')->store('avatars', 'public');
|
||||
$validated['avatar_path'] = $path;
|
||||
}
|
||||
|
||||
$user->update($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.profile', $business->slug)
|
||||
->with('success', 'Profile updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function updatePassword(Business $business, Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'current_password' => 'required|current_password',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'logout_other_sessions' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'password' => bcrypt($validated['password']),
|
||||
]);
|
||||
|
||||
// Logout other sessions if requested
|
||||
if ($request->has('logout_other_sessions')) {
|
||||
auth()->logoutOtherDevices($validated['password']);
|
||||
}
|
||||
|
||||
return redirect()->route('seller.business.settings.profile', $business->slug)
|
||||
->with('success', 'Password updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the company information settings page.
|
||||
*/
|
||||
@@ -18,21 +101,12 @@ class SettingsController extends Controller
|
||||
|
||||
/**
|
||||
* Update the company information.
|
||||
* Note: Only business_phone and business_email can be updated due to compliance requirements.
|
||||
*/
|
||||
public function updateCompanyInformation(Business $business, Request $request)
|
||||
{
|
||||
// Only allow updating business phone and email (hybrid approach)
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'dba_name' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'business_type' => 'nullable|string',
|
||||
'tin_ein' => 'nullable|string|max:20',
|
||||
'license_number' => 'nullable|string|max:255',
|
||||
'license_type' => 'nullable|string',
|
||||
'physical_address' => 'nullable|string|max:255',
|
||||
'physical_city' => 'nullable|string|max:100',
|
||||
'physical_state' => 'nullable|string|max:2',
|
||||
'physical_zipcode' => 'nullable|string|max:10',
|
||||
'business_phone' => 'nullable|string|max:20',
|
||||
'business_email' => 'nullable|email|max:255',
|
||||
]);
|
||||
@@ -41,39 +115,233 @@ class SettingsController extends Controller
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.company-information', $business->slug)
|
||||
->with('success', 'Company information updated successfully!');
|
||||
->with('success', 'Contact information updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the users management settings page.
|
||||
*/
|
||||
public function users(Business $business)
|
||||
public function users(Business $business, Request $request, \App\Services\PermissionService $permissionService)
|
||||
{
|
||||
return view('seller.settings.users', compact('business'));
|
||||
// Exclude business owner from the users list
|
||||
$query = $business->users()->where('users.id', '!=', $business->owner_user_id);
|
||||
|
||||
// Search
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 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 last login date range
|
||||
if ($request->filled('last_login_start')) {
|
||||
$query->where('last_login_at', '>=', $request->last_login_start);
|
||||
}
|
||||
if ($request->filled('last_login_end')) {
|
||||
$query->where('last_login_at', '<=', $request->last_login_end.' 23:59:59');
|
||||
}
|
||||
|
||||
$users = $query
|
||||
->withPivot('contact_type', 'is_primary', 'permissions', 'role', 'role_template')
|
||||
->with('roles')
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->paginate(15);
|
||||
|
||||
$roleTemplates = $permissionService->getRoleTemplates();
|
||||
$permissionCategories = $permissionService->getPermissionsByCategory();
|
||||
|
||||
return view('seller.settings.users', compact('business', 'users', 'roleTemplates', 'permissionCategories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the order settings page.
|
||||
* Show the form for editing a user's permissions.
|
||||
*/
|
||||
public function orders(Business $business)
|
||||
public function editUser(Business $business, \App\Models\User $user, \App\Services\PermissionService $permissionService)
|
||||
{
|
||||
return view('seller.settings.orders', compact('business'));
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Load user with pivot data
|
||||
$user = $business->users()
|
||||
->withPivot('contact_type', 'is_primary', 'permissions', 'role', 'role_template')
|
||||
->with('roles')
|
||||
->where('users.id', $user->id)
|
||||
->first();
|
||||
|
||||
$roleTemplates = $permissionService->getRoleTemplates();
|
||||
$permissionCategories = $permissionService->getPermissionsByCategory();
|
||||
$isOwner = $business->owner_user_id === $user->id;
|
||||
|
||||
return view('seller.settings.users-edit', compact('business', 'user', 'roleTemplates', 'permissionCategories', 'isOwner'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the brands management page.
|
||||
* Store a newly created user invitation.
|
||||
*/
|
||||
public function brands(Business $business)
|
||||
public function inviteUser(Business $business, Request $request)
|
||||
{
|
||||
return view('seller.settings.brands', compact('business'));
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'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',
|
||||
'is_point_of_contact' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
// Combine first and last name
|
||||
$fullName = trim($validated['first_name'].' '.$validated['last_name']);
|
||||
|
||||
// Create user and associate with business
|
||||
$user = \App\Models\User::create([
|
||||
'name' => $fullName,
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'password' => bcrypt(str()->random(32)), // Temporary password
|
||||
]);
|
||||
|
||||
// Assign role
|
||||
$user->assignRole($validated['role']);
|
||||
|
||||
// Associate with business with additional pivot data
|
||||
$business->users()->attach($user->id, [
|
||||
'role' => $validated['role'],
|
||||
'is_primary' => false,
|
||||
'contact_type' => $request->has('is_point_of_contact') ? 'primary' : null,
|
||||
]);
|
||||
|
||||
// TODO: Send invitation email with password reset link
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users', $business->slug)
|
||||
->with('success', 'User invited successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the payment settings page.
|
||||
* Update user information and permissions.
|
||||
*/
|
||||
public function payments(Business $business)
|
||||
public function updateUser(Business $business, \App\Models\User $user, Request $request)
|
||||
{
|
||||
return view('seller.settings.payments', compact('business'));
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Prevent modifying business owner
|
||||
if ($business->owner_user_id === $user->id) {
|
||||
return redirect()
|
||||
->route('seller.business.settings.users.edit', [$business->slug, $user->id])
|
||||
->with('error', 'Cannot modify business owner permissions.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'position' => 'nullable|string|max:255',
|
||||
'company' => 'nullable|string|max:255',
|
||||
'contact_type' => 'nullable|string|max:255',
|
||||
'role' => 'nullable|string|max:255',
|
||||
'role_template' => 'nullable|string|max:255',
|
||||
'permissions' => 'nullable|array',
|
||||
]);
|
||||
|
||||
// Update user data
|
||||
$user->update([
|
||||
'position' => $validated['position'] ?? null,
|
||||
'company' => $validated['company'] ?? null,
|
||||
]);
|
||||
|
||||
// Update business_user pivot data
|
||||
$business->users()->updateExistingPivot($user->id, [
|
||||
'contact_type' => $validated['contact_type'] ?? null,
|
||||
'role' => $validated['role'] ?? null,
|
||||
'role_template' => $validated['role_template'] ?? null,
|
||||
'permissions' => $validated['permissions'] ?? null,
|
||||
'permissions_updated_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users.edit', [$business->slug, $user->id])
|
||||
->with('success', 'User updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user from business.
|
||||
*/
|
||||
public function removeUser(Business $business, \App\Models\User $user)
|
||||
{
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Detach user from business
|
||||
$business->users()->detach($user->id);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users', $business->slug)
|
||||
->with('success', 'User removed successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the sales configuration page (orders + invoices).
|
||||
*/
|
||||
public function salesConfig(Business $business)
|
||||
{
|
||||
return view('seller.settings.sales-config', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sales configuration settings (orders + invoices).
|
||||
*/
|
||||
public function updateSalesConfig(Business $business, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
// Order settings
|
||||
'separate_orders_by_brand' => 'nullable|boolean',
|
||||
'auto_increment_order_ids' => 'nullable|boolean',
|
||||
'show_mark_as_paid' => 'nullable|boolean',
|
||||
'display_crm_license_on_orders' => 'nullable|boolean',
|
||||
'order_minimum' => 'nullable|numeric|min:0',
|
||||
'default_shipping_charge' => 'nullable|numeric|min:0',
|
||||
'free_shipping_minimum' => 'nullable|numeric|min:0',
|
||||
'order_disclaimer' => 'nullable|string|max:2000',
|
||||
'order_invoice_footer' => 'nullable|string|max:1000',
|
||||
'prevent_order_editing' => 'required|in:never,after_approval,after_fulfillment,always',
|
||||
'az_require_patient_count' => 'nullable|boolean',
|
||||
'az_require_allotment_verification' => 'nullable|boolean',
|
||||
// Invoice settings
|
||||
'invoice_payable_company_name' => 'nullable|string|max:255',
|
||||
'invoice_payable_address' => 'nullable|string|max:255',
|
||||
'invoice_payable_city' => 'nullable|string|max:100',
|
||||
'invoice_payable_state' => 'nullable|string|max:2',
|
||||
'invoice_payable_zipcode' => 'nullable|string|max:10',
|
||||
]);
|
||||
|
||||
// Convert checkbox values (null means unchecked)
|
||||
$validated['separate_orders_by_brand'] = $request->has('separate_orders_by_brand');
|
||||
$validated['auto_increment_order_ids'] = $request->has('auto_increment_order_ids');
|
||||
$validated['show_mark_as_paid'] = $request->has('show_mark_as_paid');
|
||||
$validated['display_crm_license_on_orders'] = $request->has('display_crm_license_on_orders');
|
||||
$validated['az_require_patient_count'] = $request->has('az_require_patient_count');
|
||||
$validated['az_require_allotment_verification'] = $request->has('az_require_allotment_verification');
|
||||
|
||||
$business->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.sales-config', $business->slug)
|
||||
->with('success', 'Sales configuration updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,12 +352,46 @@ class SettingsController extends Controller
|
||||
return view('seller.settings.invoices', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the brand kit page (Cannabrands assets/branding settings).
|
||||
*/
|
||||
public function brandKit(Business $business)
|
||||
{
|
||||
return view('seller.settings.brand-kit', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the payment settings page.
|
||||
*/
|
||||
public function payments(Business $business)
|
||||
{
|
||||
return view('seller.settings.payments', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the manage licenses page.
|
||||
*/
|
||||
public function manageLicenses(Business $business)
|
||||
{
|
||||
return view('seller.settings.manage-licenses', compact('business'));
|
||||
// Get all locations with license information
|
||||
$locations = $business->locations()
|
||||
->whereNotNull('license_number')
|
||||
->get();
|
||||
|
||||
// Check for expiring licenses (within 30 days)
|
||||
$expiringLocations = $business->locations()
|
||||
->whereNotNull('license_expiration')
|
||||
->whereDate('license_expiration', '<=', now()->addDays(30))
|
||||
->whereDate('license_expiration', '>=', now())
|
||||
->get();
|
||||
|
||||
// Check for expired licenses
|
||||
$expiredLocations = $business->locations()
|
||||
->whereNotNull('license_expiration')
|
||||
->whereDate('license_expiration', '<', now())
|
||||
->get();
|
||||
|
||||
return view('seller.settings.manage-licenses', compact('business', 'locations', 'expiringLocations', 'expiredLocations'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +402,150 @@ class SettingsController extends Controller
|
||||
return view('seller.settings.plans-and-billing', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the business subscription plan.
|
||||
*/
|
||||
public function changePlan(Business $business, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'plan_id' => 'required|in:standard,business,premium',
|
||||
]);
|
||||
|
||||
$planId = $validated['plan_id'];
|
||||
|
||||
// Define available plans with pricing
|
||||
$plans = [
|
||||
'standard' => ['name' => 'Marketplace Standard', 'price' => 99.00],
|
||||
'business' => ['name' => 'Marketplace Business', 'price' => 395.00],
|
||||
'premium' => ['name' => 'Marketplace Premium', 'price' => 795.00],
|
||||
];
|
||||
|
||||
$newPlan = $plans[$planId];
|
||||
|
||||
// Get or create subscription
|
||||
$subscription = $business->subscription()->firstOrCreate(
|
||||
['business_id' => $business->id],
|
||||
[
|
||||
'plan_id' => 'standard',
|
||||
'plan_name' => 'Marketplace Standard',
|
||||
'plan_price' => 99.00,
|
||||
'status' => 'active',
|
||||
'current_period_start' => now(),
|
||||
'current_period_end' => now()->addMonth(),
|
||||
]
|
||||
);
|
||||
|
||||
// Check if same plan
|
||||
if ($subscription->plan_id === $planId) {
|
||||
return redirect()
|
||||
->route('seller.business.settings.plans-and-billing', $business->slug)
|
||||
->with('info', 'You are already on this plan.');
|
||||
}
|
||||
|
||||
// Determine if upgrade or downgrade
|
||||
$isUpgrade = $newPlan['price'] > $subscription->plan_price;
|
||||
|
||||
if ($isUpgrade) {
|
||||
// UPGRADE: Calculate prorated charge and update immediately
|
||||
$daysLeftInCycle = now()->diffInDays($subscription->current_period_end);
|
||||
$proratedCredit = ($subscription->plan_price / 30) * $daysLeftInCycle;
|
||||
$proratedCharge = ($newPlan['price'] / 30) * $daysLeftInCycle;
|
||||
$amountToPay = $proratedCharge - $proratedCredit;
|
||||
|
||||
// Create invoice for the upgrade
|
||||
$invoiceNumber = 'INV-'.now()->format('Y').'-'.str_pad(\App\Models\SubscriptionInvoice::count() + 1, 5, '0', STR_PAD_LEFT);
|
||||
|
||||
$invoice = \App\Models\SubscriptionInvoice::create([
|
||||
'subscription_id' => $subscription->id,
|
||||
'business_id' => $business->id,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'type' => 'upgrade',
|
||||
'amount' => $amountToPay,
|
||||
'status' => 'pending',
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(7),
|
||||
'line_items' => [
|
||||
[
|
||||
'description' => "{$newPlan['name']} (prorated for {$daysLeftInCycle} days)",
|
||||
'amount' => $proratedCharge,
|
||||
],
|
||||
[
|
||||
'description' => "Credit from {$subscription->plan_name}",
|
||||
'amount' => -$proratedCredit,
|
||||
],
|
||||
],
|
||||
'payment_method_id' => $subscription->default_payment_method_id,
|
||||
]);
|
||||
|
||||
// Update subscription to new plan immediately
|
||||
$subscription->update([
|
||||
'plan_id' => $planId,
|
||||
'plan_name' => $newPlan['name'],
|
||||
'plan_price' => $newPlan['price'],
|
||||
'scheduled_plan_id' => null,
|
||||
'scheduled_plan_name' => null,
|
||||
'scheduled_plan_price' => null,
|
||||
'scheduled_change_date' => null,
|
||||
]);
|
||||
|
||||
// TODO: Charge the payment method for $amountToPay
|
||||
// TODO: Mark invoice as paid after successful charge
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.plans-and-billing', $business->slug)
|
||||
->with('success', sprintf(
|
||||
'Plan upgraded to %s! Invoice %s created for $%s (prorated). New features are active immediately.',
|
||||
$newPlan['name'],
|
||||
$invoiceNumber,
|
||||
number_format($amountToPay, 2)
|
||||
));
|
||||
|
||||
} else {
|
||||
// DOWNGRADE: Schedule for next billing cycle
|
||||
$subscription->update([
|
||||
'scheduled_plan_id' => $planId,
|
||||
'scheduled_plan_name' => $newPlan['name'],
|
||||
'scheduled_plan_price' => $newPlan['price'],
|
||||
'scheduled_change_date' => $subscription->current_period_end,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.plans-and-billing', $business->slug)
|
||||
->with('info', sprintf(
|
||||
'Plan will be downgraded to %s on %s. You\'ll continue to have access to %s features until then.',
|
||||
$newPlan['name'],
|
||||
$subscription->current_period_end->format('F j, Y'),
|
||||
$subscription->plan_name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a scheduled plan downgrade.
|
||||
*/
|
||||
public function cancelDowngrade(Business $business)
|
||||
{
|
||||
$subscription = $business->subscription;
|
||||
|
||||
if (! $subscription || ! $subscription->hasScheduledDowngrade()) {
|
||||
return redirect()
|
||||
->route('seller.business.settings.plans-and-billing', $business->slug)
|
||||
->with('error', 'No scheduled downgrade found.');
|
||||
}
|
||||
|
||||
// Cancel the scheduled downgrade
|
||||
$subscription->update([
|
||||
'scheduled_plan_id' => null,
|
||||
'scheduled_plan_name' => null,
|
||||
'scheduled_plan_price' => null,
|
||||
'scheduled_change_date' => null,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.plans-and-billing', $business->slug)
|
||||
->with('success', 'Scheduled plan downgrade has been cancelled. You will remain on your current plan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the notification preferences page.
|
||||
*/
|
||||
@@ -108,6 +554,65 @@ class SettingsController extends Controller
|
||||
return view('seller.settings.notifications', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the notification settings.
|
||||
*
|
||||
* EMAIL NOTIFICATION RULES DOCUMENTATION:
|
||||
*
|
||||
* 1. NEW ORDER EMAIL NOTIFICATIONS (new_order_email_notifications)
|
||||
* Base: Email these addresses when a new order is placed
|
||||
* - If 'new_order_only_when_no_sales_rep' checked: ONLY send if buyer has NO sales rep assigned
|
||||
* - If 'new_order_do_not_send_to_admins' checked: Do NOT send to company admins (only to these addresses)
|
||||
*
|
||||
* 2. ORDER ACCEPTED EMAIL NOTIFICATIONS (order_accepted_email_notifications)
|
||||
* Base: Email these addresses when an order is accepted
|
||||
* - If 'enable_shipped_emails_for_sales_reps' checked: Sales reps assigned to customer get email when order marked Shipped
|
||||
*
|
||||
* 3. PLATFORM INQUIRY EMAIL NOTIFICATIONS (platform_inquiry_email_notifications)
|
||||
* Base: Email these addresses for inquiries
|
||||
* - Sales reps associated with customer ALWAYS receive email
|
||||
* - If field is blank AND no sales reps exist: company admins receive notifications
|
||||
*
|
||||
* 4. MANUAL ORDER EMAIL NOTIFICATIONS
|
||||
* - If 'enable_manual_order_email_notifications' checked: Send same emails for manual orders as buyer-created orders
|
||||
* - If 'enable_manual_order_email_notifications' unchecked: Only send for buyer-created orders
|
||||
* - If 'manual_order_emails_internal_only' checked: Send manual order emails to internal recipients only (not buyers)
|
||||
*
|
||||
* 5. LOW INVENTORY EMAIL NOTIFICATIONS (low_inventory_email_notifications)
|
||||
* Base: Email these addresses when inventory is low
|
||||
*
|
||||
* 6. CERTIFIED SELLER STATUS EMAIL NOTIFICATIONS (certified_seller_status_email_notifications)
|
||||
* Base: Email these addresses when seller status changes
|
||||
*/
|
||||
public function updateNotifications(Business $business, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'new_order_email_notifications' => ['nullable', 'string', new \App\Rules\CommaSeparatedEmails],
|
||||
'new_order_only_when_no_sales_rep' => 'nullable|boolean',
|
||||
'new_order_do_not_send_to_admins' => 'nullable|boolean',
|
||||
'order_accepted_email_notifications' => ['nullable', 'string', new \App\Rules\CommaSeparatedEmails],
|
||||
'enable_shipped_emails_for_sales_reps' => 'nullable|boolean',
|
||||
'platform_inquiry_email_notifications' => ['nullable', 'string', new \App\Rules\CommaSeparatedEmails],
|
||||
'enable_manual_order_email_notifications' => 'nullable|boolean',
|
||||
'manual_order_emails_internal_only' => 'nullable|boolean',
|
||||
'low_inventory_email_notifications' => ['nullable', 'string', new \App\Rules\CommaSeparatedEmails],
|
||||
'certified_seller_status_email_notifications' => ['nullable', 'string', new \App\Rules\CommaSeparatedEmails],
|
||||
]);
|
||||
|
||||
// Convert checkbox values (null means unchecked)
|
||||
$validated['new_order_only_when_no_sales_rep'] = $request->has('new_order_only_when_no_sales_rep');
|
||||
$validated['new_order_do_not_send_to_admins'] = $request->has('new_order_do_not_send_to_admins');
|
||||
$validated['enable_shipped_emails_for_sales_reps'] = $request->has('enable_shipped_emails_for_sales_reps');
|
||||
$validated['enable_manual_order_email_notifications'] = $request->has('enable_manual_order_email_notifications');
|
||||
$validated['manual_order_emails_internal_only'] = $request->has('manual_order_emails_internal_only');
|
||||
|
||||
$business->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.notifications', $business->slug)
|
||||
->with('success', 'Notification settings updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the report settings page.
|
||||
*/
|
||||
@@ -115,4 +620,157 @@ class SettingsController extends Controller
|
||||
{
|
||||
return view('seller.settings.reports', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the integrations page.
|
||||
*/
|
||||
public function integrations(Business $business)
|
||||
{
|
||||
return view('seller.settings.integrations', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the webhooks / API page.
|
||||
*/
|
||||
public function webhooks(Business $business)
|
||||
{
|
||||
return view('seller.settings.webhooks', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the audit logs page.
|
||||
*/
|
||||
public function auditLogs(Business $business, Request $request)
|
||||
{
|
||||
// CRITICAL: Only show audit logs for THIS business (multi-tenancy)
|
||||
$query = \App\Models\AuditLog::forBusiness($business->id)
|
||||
->with(['user', 'auditable']);
|
||||
|
||||
// Search filter
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('description', 'like', "%{$search}%")
|
||||
->orWhere('event', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($userQuery) use ($search) {
|
||||
$userQuery->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by event type
|
||||
if ($request->filled('event')) {
|
||||
$query->byEvent($request->event);
|
||||
}
|
||||
|
||||
// Filter by auditable type (resource type)
|
||||
if ($request->filled('type')) {
|
||||
$query->byType($request->type);
|
||||
}
|
||||
|
||||
// Filter by user
|
||||
if ($request->filled('user_id')) {
|
||||
$query->forUser($request->user_id);
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if ($request->filled('start_date')) {
|
||||
$query->where('created_at', '>=', $request->start_date);
|
||||
}
|
||||
if ($request->filled('end_date')) {
|
||||
$query->where('created_at', '<=', $request->end_date.' 23:59:59');
|
||||
}
|
||||
|
||||
// Get paginated results, ordered by most recent first
|
||||
$audits = $query->latest('created_at')->paginate(50);
|
||||
|
||||
// Get unique event types for filter dropdown
|
||||
$eventTypes = \App\Models\AuditLog::forBusiness($business->id)
|
||||
->select('event')
|
||||
->distinct()
|
||||
->pluck('event')
|
||||
->sort();
|
||||
|
||||
// Get unique auditable types for filter dropdown
|
||||
$auditableTypes = \App\Models\AuditLog::forBusiness($business->id)
|
||||
->select('auditable_type')
|
||||
->whereNotNull('auditable_type')
|
||||
->distinct()
|
||||
->get()
|
||||
->map(function ($log) {
|
||||
$parts = explode('\\', $log->auditable_type);
|
||||
|
||||
return end($parts);
|
||||
})
|
||||
->unique()
|
||||
->sort();
|
||||
|
||||
return view('seller.settings.audit-logs', compact('business', 'audits', 'eventTypes', 'auditableTypes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* View an invoice.
|
||||
*/
|
||||
public function viewInvoice(Business $business, string $invoiceId)
|
||||
{
|
||||
// TODO: Fetch actual invoice from database
|
||||
$invoice = [
|
||||
'id' => $invoiceId,
|
||||
'date' => now()->subDays(rand(1, 90)),
|
||||
'amount' => 395.00,
|
||||
'status' => 'paid',
|
||||
'items' => [
|
||||
['description' => 'Marketplace Business Plan', 'quantity' => 1, 'price' => 395.00],
|
||||
],
|
||||
];
|
||||
|
||||
return view('seller.settings.invoice-view', compact('business', 'invoice'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an invoice as PDF.
|
||||
*/
|
||||
public function downloadInvoice(Business $business, string $invoiceId)
|
||||
{
|
||||
// TODO: Generate actual PDF from invoice data
|
||||
// For now, return a mock PDF
|
||||
|
||||
$invoice = [
|
||||
'id' => $invoiceId,
|
||||
'date' => now()->subDays(rand(1, 90)),
|
||||
'amount' => 395.00,
|
||||
'status' => 'paid',
|
||||
'business_name' => $business->name,
|
||||
'business_address' => $business->physical_address,
|
||||
];
|
||||
|
||||
// Generate a simple mock PDF content
|
||||
$pdfContent = "INVOICE #{$invoice['id']}\n\n";
|
||||
$pdfContent .= "Date: {$invoice['date']->format('m/d/Y')}\n";
|
||||
$pdfContent .= "Business: {$invoice['business_name']}\n";
|
||||
$pdfContent .= 'Amount: $'.number_format($invoice['amount'], 2)."\n";
|
||||
$pdfContent .= 'Status: '.strtoupper($invoice['status'])."\n\n";
|
||||
$pdfContent .= "This is a mock invoice for testing purposes.\n";
|
||||
$pdfContent .= "In production, this would be a properly formatted PDF.\n";
|
||||
|
||||
return response($pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'attachment; filename="invoice-'.$invoiceId.'.pdf"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the current view (sales, manufacturing, compliance).
|
||||
*/
|
||||
public function switchView(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'view' => 'required|in:sales,manufacturing,compliance',
|
||||
]);
|
||||
|
||||
session(['current_view' => $validated['view']]);
|
||||
|
||||
return redirect()->back()->with('success', 'View switched successfully');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Traits\FileStorageHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class StorageTestController extends Controller
|
||||
{
|
||||
use FileStorageHelper;
|
||||
|
||||
/**
|
||||
* Test storage configuration
|
||||
*/
|
||||
public function test(Request $request)
|
||||
{
|
||||
$results = [];
|
||||
$results['storage_info'] = $this->getStorageInfo();
|
||||
|
||||
// Test file upload if provided
|
||||
if ($request->hasFile('test_file')) {
|
||||
try {
|
||||
$file = $request->file('test_file');
|
||||
|
||||
// Store test file
|
||||
$path = $this->storeFile($file, 'tests');
|
||||
$results['upload'] = [
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
'url' => $this->getFileUrl($path),
|
||||
];
|
||||
|
||||
// Verify file exists
|
||||
$disk = Storage::disk($this->getStorageDisk());
|
||||
$results['verification'] = [
|
||||
'exists' => $disk->exists($path),
|
||||
'size' => $disk->size($path),
|
||||
];
|
||||
|
||||
// Delete test file
|
||||
$deleted = $this->deleteFile($path);
|
||||
$results['cleanup'] = [
|
||||
'deleted' => $deleted,
|
||||
'still_exists' => $disk->exists($path),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results['error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($results, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show test upload form
|
||||
*/
|
||||
public function form()
|
||||
{
|
||||
return view('storage-test');
|
||||
}
|
||||
}
|
||||
70
app/Models/AuditLog.php
Normal file
70
app/Models/AuditLog.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use OwenIt\Auditing\Models\Audit;
|
||||
|
||||
/**
|
||||
* AuditLog Model
|
||||
*
|
||||
* Wrapper around the Laravel Auditing package's Audit model
|
||||
* with business-specific scopes and relationships for multi-tenancy.
|
||||
*/
|
||||
class AuditLog extends Audit
|
||||
{
|
||||
/**
|
||||
* Scope to filter audits for a specific business
|
||||
*
|
||||
* Since the audits table doesn't have a business_id column,
|
||||
* we filter by the auditable models that belong to the business.
|
||||
* For now, we'll show all audits - this can be refined later
|
||||
* when implementing proper multi-tenant audit filtering.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $businessId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeForBusiness($query, $businessId)
|
||||
{
|
||||
// TODO: Implement proper business-scoped filtering
|
||||
// This would require joining with auditable models to check business ownership
|
||||
// For now, return all audits (will be implemented when audit system is fully configured)
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by event type
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $event
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByEvent($query, $event)
|
||||
{
|
||||
return $query->where('event', $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by auditable type (resource type)
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $type
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByType($query, $type)
|
||||
{
|
||||
return $query->where('auditable_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter audits for a specific user
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $userId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeForUser($query, $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
}
|
||||
43
app/Models/ComponentCategory.php
Normal file
43
app/Models/ComponentCategory.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ComponentCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'business_id',
|
||||
'name',
|
||||
'description',
|
||||
'slug',
|
||||
'sort_order',
|
||||
'parent_id',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function business(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Business::class);
|
||||
}
|
||||
|
||||
public function components(): HasMany
|
||||
{
|
||||
return $this->hasMany(Component::class, 'component_category_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ComponentCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(ComponentCategory::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
43
app/Models/ProductCategory.php
Normal file
43
app/Models/ProductCategory.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'business_id',
|
||||
'name',
|
||||
'description',
|
||||
'slug',
|
||||
'sort_order',
|
||||
'parent_id',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function business(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Business::class);
|
||||
}
|
||||
|
||||
public function products(): HasMany
|
||||
{
|
||||
return $this->hasMany(Product::class, 'product_category_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ class ProductImage extends Model
|
||||
'path',
|
||||
'type',
|
||||
'order',
|
||||
'sort_order',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function product(): BelongsTo
|
||||
|
||||
202
app/Services/PermissionService.php
Normal file
202
app/Services/PermissionService.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/**
|
||||
* Permission Service
|
||||
*
|
||||
* Handles role templates and permission management for businesses.
|
||||
*/
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* Get available role templates
|
||||
*/
|
||||
public function getRoleTemplates(): array
|
||||
{
|
||||
return [
|
||||
'admin' => [
|
||||
'name' => 'Administrator',
|
||||
'description' => 'Full access to all features and settings',
|
||||
'permissions' => ['*'],
|
||||
],
|
||||
'manager' => [
|
||||
'name' => 'Manager',
|
||||
'description' => 'Manage products, orders, and basic settings',
|
||||
'permissions' => [
|
||||
'products.view',
|
||||
'products.create',
|
||||
'products.edit',
|
||||
'products.delete',
|
||||
'orders.view',
|
||||
'orders.edit',
|
||||
'inventory.view',
|
||||
'inventory.edit',
|
||||
],
|
||||
],
|
||||
'sales' => [
|
||||
'name' => 'Sales Representative',
|
||||
'description' => 'View and manage orders',
|
||||
'permissions' => [
|
||||
'products.view',
|
||||
'orders.view',
|
||||
'orders.create',
|
||||
'orders.edit',
|
||||
],
|
||||
],
|
||||
'viewer' => [
|
||||
'name' => 'Viewer',
|
||||
'description' => 'View-only access to products and orders',
|
||||
'permissions' => [
|
||||
'products.view',
|
||||
'orders.view',
|
||||
'inventory.view',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions organized by category
|
||||
*/
|
||||
public function getPermissionsByCategory(): array
|
||||
{
|
||||
return [
|
||||
'products' => [
|
||||
'name' => 'Products',
|
||||
'icon' => 'lucide--package',
|
||||
'permissions' => [
|
||||
'products.view' => [
|
||||
'name' => 'View Products',
|
||||
'description' => 'View product catalog and details',
|
||||
],
|
||||
'products.create' => [
|
||||
'name' => 'Create Products',
|
||||
'description' => 'Add new products to the catalog',
|
||||
],
|
||||
'products.edit' => [
|
||||
'name' => 'Edit Products',
|
||||
'description' => 'Modify existing product information',
|
||||
],
|
||||
'products.delete' => [
|
||||
'name' => 'Delete Products',
|
||||
'description' => 'Remove products from the catalog',
|
||||
],
|
||||
],
|
||||
],
|
||||
'orders' => [
|
||||
'name' => 'Orders',
|
||||
'icon' => 'lucide--shopping-cart',
|
||||
'permissions' => [
|
||||
'orders.view' => [
|
||||
'name' => 'View Orders',
|
||||
'description' => 'View order history and details',
|
||||
],
|
||||
'orders.create' => [
|
||||
'name' => 'Create Orders',
|
||||
'description' => 'Place new orders',
|
||||
],
|
||||
'orders.edit' => [
|
||||
'name' => 'Edit Orders',
|
||||
'description' => 'Modify existing orders',
|
||||
],
|
||||
'orders.delete' => [
|
||||
'name' => 'Cancel Orders',
|
||||
'description' => 'Cancel or delete orders',
|
||||
],
|
||||
],
|
||||
],
|
||||
'inventory' => [
|
||||
'name' => 'Inventory',
|
||||
'icon' => 'lucide--warehouse',
|
||||
'permissions' => [
|
||||
'inventory.view' => [
|
||||
'name' => 'View Inventory',
|
||||
'description' => 'View inventory levels and locations',
|
||||
],
|
||||
'inventory.edit' => [
|
||||
'name' => 'Edit Inventory',
|
||||
'description' => 'Adjust inventory quantities',
|
||||
],
|
||||
'inventory.transfer' => [
|
||||
'name' => 'Transfer Inventory',
|
||||
'description' => 'Move inventory between locations',
|
||||
],
|
||||
],
|
||||
],
|
||||
'customers' => [
|
||||
'name' => 'Customers',
|
||||
'icon' => 'lucide--users',
|
||||
'permissions' => [
|
||||
'customers.view' => [
|
||||
'name' => 'View Customers',
|
||||
'description' => 'View customer profiles and information',
|
||||
],
|
||||
'customers.create' => [
|
||||
'name' => 'Create Customers',
|
||||
'description' => 'Add new customer accounts',
|
||||
],
|
||||
'customers.edit' => [
|
||||
'name' => 'Edit Customers',
|
||||
'description' => 'Modify customer information',
|
||||
],
|
||||
],
|
||||
],
|
||||
'settings' => [
|
||||
'name' => 'Settings',
|
||||
'icon' => 'lucide--settings',
|
||||
'permissions' => [
|
||||
'settings.view' => [
|
||||
'name' => 'View Settings',
|
||||
'description' => 'View business settings and configuration',
|
||||
],
|
||||
'settings.edit' => [
|
||||
'name' => 'Edit Settings',
|
||||
'description' => 'Modify business settings',
|
||||
],
|
||||
'users.manage' => [
|
||||
'name' => 'Manage Users',
|
||||
'description' => 'Add, edit, and remove user accounts',
|
||||
],
|
||||
'billing.manage' => [
|
||||
'name' => 'Manage Billing',
|
||||
'description' => 'View and manage billing information',
|
||||
],
|
||||
'audit-logs.view' => [
|
||||
'name' => 'View Audit Logs',
|
||||
'description' => 'Access audit logs and activity history',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available permissions (flattened)
|
||||
*/
|
||||
public function getAllPermissions(): array
|
||||
{
|
||||
$permissions = [];
|
||||
foreach ($this->getPermissionsByCategory() as $category => $categoryData) {
|
||||
$permissions = array_merge($permissions, array_keys($categoryData['permissions']));
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a specific permission
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
*/
|
||||
public function hasPermission($user, string $permission): bool
|
||||
{
|
||||
// If user has wildcard permission, grant access to everything
|
||||
if (is_array($user->permissions) && in_array('*', $user->permissions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has the specific permission
|
||||
return is_array($user->permissions) && in_array($permission, $user->permissions);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const content = fs.readFileSync('resources/views/seller/products/edit11.blade.php', 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let depth = 0;
|
||||
const stack = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const lineNum = i + 1;
|
||||
|
||||
// Skip lines that are Alpine.js @error handlers
|
||||
if (line.includes('@error') && line.includes('$event')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for @if (but not in @endif, @error, @enderror)
|
||||
if (/@if\s*\(/.test(line) && !/@endif/.test(line)) {
|
||||
depth++;
|
||||
stack.push({ line: lineNum, type: 'if', content: line.trim().substring(0, 80) });
|
||||
console.log(`${lineNum}: [depth +${depth}] @if`);
|
||||
}
|
||||
|
||||
// Check for @elseif
|
||||
if (/@elseif\s*\(/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @elseif`);
|
||||
}
|
||||
|
||||
// Check for @else (but not @elseif, @endforelse, @enderror)
|
||||
if (/@else\b/.test(line) && !/@elseif/.test(line) && !/@endforelse/.test(line) && !/@enderror/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @else`);
|
||||
}
|
||||
|
||||
// Check for @endif
|
||||
if (/@endif\b/.test(line)) {
|
||||
console.log(`${lineNum}: [depth -${depth}] @endif`);
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
stack.pop();
|
||||
} else {
|
||||
console.log(`ERROR: Extra @endif at line ${lineNum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFinal depth: ${depth}`);
|
||||
if (depth > 0) {
|
||||
console.log(`\nUNBALANCED: Missing ${depth} @endif statement(s)`);
|
||||
console.log('\nUnclosed @if statements:');
|
||||
stack.forEach(item => {
|
||||
console.log(` Line ${item.line}: ${item.content}`);
|
||||
});
|
||||
} else {
|
||||
console.log('\nAll @if/@endif pairs are balanced!');
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
$file = 'C:\Users\Boss Man\Documents\GitHub\hub\resources\views\seller\products\edit11.blade.php';
|
||||
$lines = file($file);
|
||||
|
||||
$stack = [];
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
$lineNum++; // 1-indexed
|
||||
|
||||
// Check for @if (but not @endif, @elseif, etc.)
|
||||
if (preg_match('/^\s*@if\(/', $line)) {
|
||||
$stack[] = ['type' => 'if', 'line' => $lineNum];
|
||||
echo "Line $lineNum: OPEN @if (stack depth: ".count($stack).")\n";
|
||||
}
|
||||
// Check for @elseif
|
||||
elseif (preg_match('/^\s*@elseif\(/', $line)) {
|
||||
echo "Line $lineNum: @elseif\n";
|
||||
}
|
||||
// Check for @else
|
||||
elseif (preg_match('/^\s*@else\s*$/', $line)) {
|
||||
echo "Line $lineNum: @else\n";
|
||||
}
|
||||
// Check for @endif
|
||||
elseif (preg_match('/^\s*@endif\s*$/', $line)) {
|
||||
if (empty($stack)) {
|
||||
echo "ERROR Line $lineNum: @endif without matching @if!\n";
|
||||
} else {
|
||||
$opened = array_pop($stack);
|
||||
echo "Line $lineNum: CLOSE @endif (opened at line {$opened['line']}, stack depth: ".count($stack).")\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($stack)) {
|
||||
echo "\nERROR: Unclosed @if directives:\n";
|
||||
foreach ($stack as $item) {
|
||||
echo " Line {$item['line']}: @if never closed\n";
|
||||
}
|
||||
} else {
|
||||
echo "\nAll @if/@endif directives are balanced!\n";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('businesses', function (Blueprint $table) {
|
||||
$table->boolean('has_manufacturing')->default(false)->after('status');
|
||||
$table->boolean('has_compliance')->default(false)->after('has_manufacturing');
|
||||
$table->boolean('has_marketing')->default(false)->after('has_compliance');
|
||||
$table->boolean('has_analytics')->default(false)->after('has_marketing');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('businesses', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'has_manufacturing',
|
||||
'has_compliance',
|
||||
'has_marketing',
|
||||
'has_analytics',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
408
docs/ROUTE_ISOLATION.md
Normal file
408
docs/ROUTE_ISOLATION.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# Route Isolation & Module Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The application has three completely isolated route areas to prevent development collisions and maintain clear separation of concerns.
|
||||
|
||||
## Route Areas
|
||||
|
||||
### 1. Superadmin Platform (`/admin`)
|
||||
- **Framework**: Filament v3
|
||||
- **Users**: Platform superadmins
|
||||
- **Scope**: Platform-wide (manages all businesses, users, and system configuration)
|
||||
- **Routing**: Auto-generated by Filament Resources
|
||||
- **Isolation**: Each Resource is a separate file
|
||||
|
||||
**Filament Resources:**
|
||||
- UserResource - Platform user management
|
||||
- BusinessResource - All businesses (buyers + sellers)
|
||||
- BusinessModuleResource - Enable/disable modules per business
|
||||
- BrandResource - Brand management
|
||||
- ProductResource - Product management
|
||||
- ComponentResource - Component management
|
||||
- OrderResource - Platform-wide order oversight
|
||||
- InvoiceResource - Platform-wide invoice oversight
|
||||
- ModuleResource - Module configuration
|
||||
- Batches/BatchResource - Manufacturing batch oversight
|
||||
|
||||
**Development Pattern:**
|
||||
- No route collision risk (Filament auto-generates routes)
|
||||
- Developers work on separate Resource files
|
||||
- Already isolated by design
|
||||
|
||||
---
|
||||
|
||||
### 2. Buyer/Dispensary Routes (`/b/{business}/*`)
|
||||
- **Framework**: Blade + DaisyUI + Tailwind
|
||||
- **Users**: Dispensary owners, purchasing managers, staff
|
||||
- **Scope**: Business-scoped (multi-tenant)
|
||||
- **Routing**: Custom Laravel route groups
|
||||
|
||||
**Route Structure:**
|
||||
```
|
||||
/b/ # User-level (NO business context)
|
||||
/register # Registration flow
|
||||
/profile # Personal profile
|
||||
/settings # Personal settings
|
||||
/dashboard # Marketplace browsing
|
||||
/browse # Product marketplace
|
||||
/brands # Brand directory
|
||||
|
||||
/b/{business}/ # Business-scoped routes
|
||||
/cart # Shopping cart
|
||||
/checkout # Checkout process
|
||||
/orders # Order management
|
||||
/invoices # Invoice management
|
||||
/favorites # Favorites/wishlists
|
||||
/settings/* # Business settings (MODULE)
|
||||
```
|
||||
|
||||
**Buyer Settings Module** (`/b/{business}/settings/*`):
|
||||
- Always enabled (required module)
|
||||
- Access controlled via role-based permissions
|
||||
- Features: Profile, locations, contacts, team management
|
||||
|
||||
---
|
||||
|
||||
### 3. Seller/Brand Routes (`/s/{business}/*`)
|
||||
- **Framework**: Blade + DaisyUI + Tailwind
|
||||
- **Users**: Brand owners, production managers, sales staff
|
||||
- **Scope**: Business-scoped (multi-tenant)
|
||||
- **Routing**: Custom Laravel route groups with module isolation
|
||||
|
||||
**Route Structure:**
|
||||
```
|
||||
/s/ # User-level (NO business context)
|
||||
/register # Registration flow
|
||||
/profile # Personal profile
|
||||
/settings # Personal settings
|
||||
/dashboard # Main dashboard
|
||||
/setup # Business onboarding wizard
|
||||
|
||||
/s/{business}/ # Business-scoped routes (CORE - always enabled)
|
||||
/dashboard # Business dashboard
|
||||
/orders # Order fulfillment
|
||||
/invoices # Invoice management
|
||||
/customers # CRM
|
||||
/products # Product catalog
|
||||
/components # Component management
|
||||
/locations # Location management
|
||||
/contacts # Contact management
|
||||
/fleet/drivers # Driver management
|
||||
/fleet/vehicles # Vehicle management
|
||||
|
||||
/s/{business}/manufacturing/* # OPTIONAL MODULE
|
||||
/s/{business}/compliance/* # OPTIONAL MODULE
|
||||
/s/{business}/marketing/* # OPTIONAL MODULE
|
||||
/s/{business}/analytics/* # OPTIONAL MODULE
|
||||
/s/{business}/settings/* # REQUIRED MODULE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Platform vs. Modules
|
||||
|
||||
### Core B2B Platform (NOT a module - base application)
|
||||
The foundation of Cannabrands B2B marketplace:
|
||||
|
||||
**Buyer Side (`/b/{business}/*`):**
|
||||
- Product browsing and marketplace
|
||||
- Shopping cart and checkout
|
||||
- Order management
|
||||
- Invoice tracking
|
||||
- Basic analytics (order history, purchase trends)
|
||||
|
||||
**Seller Side (`/s/{business}/*`):**
|
||||
- Product catalog management
|
||||
- Order fulfillment
|
||||
- Invoice generation
|
||||
- Customer management (CRM)
|
||||
- Fleet management (drivers, vehicles)
|
||||
- **Core analytics** (sales dashboards, order reports, product performance)
|
||||
|
||||
**Core Analytics** (built into platform, no module flag needed):
|
||||
- Sales dashboards with revenue trends
|
||||
- Order history charts and filtering
|
||||
- Product performance metrics
|
||||
- Customer analytics and trends
|
||||
- Basic KPIs on main dashboard
|
||||
- Standard report exports
|
||||
|
||||
**Permissions for core analytics:**
|
||||
- `view-dashboard` - See basic sales dashboard
|
||||
- `view-order-reports` - See order history charts
|
||||
- `view-sales-metrics` - See revenue/sales KPIs
|
||||
- `export-order-data` - Export order reports
|
||||
|
||||
---
|
||||
|
||||
## Module Types
|
||||
|
||||
### Optional Modules (Feature Flags)
|
||||
Enabled/disabled per business via database flags in `businesses` table.
|
||||
|
||||
| Module | Route Prefix | Flag | Purpose |
|
||||
|--------|-------------|------|---------|
|
||||
| **Manufacturing** | `/manufacturing/*` | `has_manufacturing` | Production tracking, batches, wash reports, conversions, BOMs |
|
||||
| **Compliance** | `/compliance/*` | `has_compliance` | Regulatory tracking, METRC integration, lab results, quarantine |
|
||||
| **Marketing** | `/marketing/*` | `has_marketing` | Social media, campaigns, email marketing, promotions, segments |
|
||||
| **Analytics** | `/analytics/*` | `has_analytics` | **Advanced BI**: Cross-module reporting, custom report builder, executive dashboards, data exports/API |
|
||||
|
||||
**Access Control:**
|
||||
- Middleware checks business flag: `if (!$business->has_manufacturing) abort(404)`
|
||||
- Only show menu items when module enabled
|
||||
- Can be enabled/disabled via `/admin` by superadmins
|
||||
|
||||
**Analytics Module Permissions** (requires `has_analytics` flag):
|
||||
- `analytics.view-advanced-reports` - Access Analytics module
|
||||
- `analytics.create-custom-reports` - Build custom reports
|
||||
- `analytics.view-cross-module` - See combined manufacturing+sales+marketing data
|
||||
- `analytics.export-bi-data` - Export BI datasets
|
||||
- `analytics.manage-dashboards` - Configure executive dashboards
|
||||
|
||||
---
|
||||
|
||||
### Required Modules (Role-Based Permissions)
|
||||
Always enabled, access controlled via Spatie permissions.
|
||||
|
||||
| Module | Route Prefix | Access Control | Purpose |
|
||||
|--------|-------------|----------------|---------|
|
||||
| **Settings** | `/settings/*` | Roles/Permissions | Company info, team management, billing, licenses, integrations |
|
||||
|
||||
**Seller Settings Routes:**
|
||||
```
|
||||
/s/{business}/settings/
|
||||
/company-information # Company details
|
||||
/users # Team management
|
||||
/orders # Order settings
|
||||
/brands # Brand settings
|
||||
/payments # Payment configuration
|
||||
/invoices # Invoice settings
|
||||
/manage-licenses # License management
|
||||
/plans-and-billing # Subscription & billing
|
||||
/notifications # Notification preferences
|
||||
/reports # Report configuration
|
||||
```
|
||||
|
||||
**Buyer Settings Routes:**
|
||||
```
|
||||
/b/{business}/settings/
|
||||
/profile # Business profile
|
||||
/locations # Location management
|
||||
/contacts # Contact management
|
||||
/users # Team management
|
||||
```
|
||||
|
||||
**Access Control:**
|
||||
- No feature flag (always available)
|
||||
- Controlled via Spatie permissions: `manage-settings`, `view-billing`, `manage-team`, etc.
|
||||
- Different roles see different settings sections
|
||||
|
||||
---
|
||||
|
||||
## Core Analytics vs. Analytics Module
|
||||
|
||||
### When to Use Core Analytics (Built-in)
|
||||
Use the built-in analytics features in the core platform when you need:
|
||||
- Standard sales dashboards
|
||||
- Order history and trends
|
||||
- Product performance metrics
|
||||
- Customer purchase analytics
|
||||
- Basic reporting and exports
|
||||
|
||||
**No module flag required** - Available to all businesses by default
|
||||
**Access controlled via core permissions** - `view-dashboard`, `view-sales-metrics`, etc.
|
||||
|
||||
### When to Use Analytics Module (Optional)
|
||||
Enable the Analytics module (`has_analytics` flag) when you need:
|
||||
- **Cross-module reporting**: Combine data from sales, manufacturing, marketing, and compliance
|
||||
- **Custom report builder**: Create ad-hoc reports with drag-and-drop
|
||||
- **Executive dashboards**: High-level KPIs across entire business
|
||||
- **Advanced visualizations**: Custom charts, heatmaps, forecasting
|
||||
- **Data exports/API**: Integrate with external BI tools (Tableau, PowerBI)
|
||||
- **Manufacturing analytics**: Track batch yields, production efficiency, waste analysis
|
||||
- **Marketing ROI**: Track campaign performance, customer acquisition costs
|
||||
|
||||
**Example:**
|
||||
- Core sales dashboard shows: "You sold 100 units this month for $10,000 revenue"
|
||||
- Analytics module shows: "Your marketing campaign cost $500, brought 50 new customers, who bought products made from 3 batches with 92% average yield, generating $6,000 profit after COGS"
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Parallel Development Without Collisions
|
||||
|
||||
**Scenario:** Three developers working simultaneously on seller features.
|
||||
|
||||
**Developer A (Sales Team):**
|
||||
- Works on: `/s/{business}/orders/*`, `/s/{business}/invoices/*`
|
||||
- Files: `routes/seller.php` (lines 126-163), `OrderController.php`, `InvoiceController.php`
|
||||
- Branch: `feature/order-enhancements`
|
||||
|
||||
**Developer B (Manufacturing Team):**
|
||||
- Works on: `/s/{business}/manufacturing/*`
|
||||
- Files: `routes/seller.php` (lines 237-240), `Manufacturing/*Controller.php`
|
||||
- Branch: `feature/wash-report-enhancements`
|
||||
|
||||
**Developer C (Settings Team):**
|
||||
- Works on: `/s/{business}/settings/*`
|
||||
- Files: `routes/seller.php` (lines 269-281), `Seller/SettingsController.php`
|
||||
- Branch: `feature/billing-integration`
|
||||
|
||||
**Result:** Zero route collisions! Each developer works in isolated route namespace.
|
||||
|
||||
---
|
||||
|
||||
## Migration: Module Flags
|
||||
|
||||
**File:** `database/migrations/2025_11_12_180802_add_module_flags_to_businesses_table.php`
|
||||
|
||||
Adds optional module flags to `businesses` table:
|
||||
|
||||
```php
|
||||
$table->boolean('has_manufacturing')->default(false);
|
||||
$table->boolean('has_compliance')->default(false);
|
||||
$table->boolean('has_marketing')->default(false);
|
||||
$table->boolean('has_analytics')->default(false);
|
||||
```
|
||||
|
||||
**Usage in Controllers:**
|
||||
```php
|
||||
// Check if business has manufacturing module enabled
|
||||
if (!$business->has_manufacturing) {
|
||||
abort(404, 'Manufacturing module not enabled for this business');
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Blade:**
|
||||
```blade
|
||||
@if($business->has_manufacturing)
|
||||
<a href="{{ route('seller.business.manufacturing.batches.index', $business) }}">
|
||||
Manufacturing
|
||||
</a>
|
||||
@endif
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Naming Convention
|
||||
|
||||
All routes use dot notation with prefixes:
|
||||
|
||||
**Admin:**
|
||||
- `admin.*` - Auto-generated by Filament
|
||||
|
||||
**Buyer:**
|
||||
- `buyer.*` - User-level routes (no business context)
|
||||
- `buyer.business.*` - Business-scoped routes
|
||||
|
||||
**Seller:**
|
||||
- `seller.*` - User-level routes (no business context)
|
||||
- `seller.business.*` - Business-scoped routes
|
||||
- `seller.business.manufacturing.*` - Manufacturing module
|
||||
- `seller.business.compliance.*` - Compliance module
|
||||
- `seller.business.marketing.*` - Marketing module
|
||||
- `seller.business.analytics.*` - Analytics module
|
||||
- `seller.business.settings.*` - Settings module
|
||||
|
||||
**Example:**
|
||||
```php
|
||||
route('seller.business.manufacturing.wash-reports.show', [$business, $washReport])
|
||||
// Generates: /s/{business-slug}/manufacturing/wash-reports/WR-123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Isolation Benefits
|
||||
|
||||
### 1. Parallel Development
|
||||
- Multiple teams work simultaneously without conflicts
|
||||
- Each module has isolated route namespace
|
||||
- Merge conflicts limited to route file only (easy to resolve)
|
||||
|
||||
### 2. Feature Enablement
|
||||
- Businesses only pay for modules they need
|
||||
- Module flags control availability per business
|
||||
- Clean separation between core and optional features
|
||||
|
||||
### 3. Permission Management
|
||||
- Role-based access within each module
|
||||
- Fine-grained control: user can access orders but not billing
|
||||
- Consistent permission naming: `manufacturing.view-batches`, `settings.manage-billing`
|
||||
|
||||
### 4. Code Organization
|
||||
- Clear boundaries between functional areas
|
||||
- Easy to locate controllers, views, tests for specific features
|
||||
- New developers quickly understand structure
|
||||
|
||||
### 5. Testing Isolation
|
||||
- Test suite can run module tests independently
|
||||
- Feature tests scoped to module routes
|
||||
- Easier to mock module availability
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Module
|
||||
|
||||
**Example: Adding Inventory module to sellers**
|
||||
|
||||
1. **Add feature flag migration:**
|
||||
```php
|
||||
Schema::table('businesses', function (Blueprint $table) {
|
||||
$table->boolean('has_inventory')->default(false);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Add route group in `routes/seller.php`:**
|
||||
```php
|
||||
// Inventory Module (Optional)
|
||||
// Flag: has_inventory
|
||||
// Features: Stock tracking, reorder points, warehouse management
|
||||
Route::prefix('inventory')->name('inventory.')->group(function () {
|
||||
Route::get('/', [InventoryController::class, 'index'])->name('index');
|
||||
Route::get('/warehouses', [InventoryController::class, 'warehouses'])->name('warehouses');
|
||||
// ... more routes
|
||||
});
|
||||
```
|
||||
|
||||
3. **Create controllers:**
|
||||
```
|
||||
app/Http/Controllers/Seller/Inventory/
|
||||
InventoryController.php
|
||||
WarehouseController.php
|
||||
```
|
||||
|
||||
4. **Add middleware check in controller:**
|
||||
```php
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(function ($request, $next) {
|
||||
$business = $request->route('business');
|
||||
|
||||
if (!$business->has_inventory) {
|
||||
abort(404, 'Inventory module not enabled');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
5. **Update navigation blade components:**
|
||||
```blade
|
||||
@if($business->has_inventory)
|
||||
<li><a href="{{ route('seller.business.inventory.index', $business) }}">Inventory</a></li>
|
||||
@endif
|
||||
```
|
||||
|
||||
Done! New module fully isolated from existing code.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `docs/URL_STRUCTURE.md` - URL patterns and conventions
|
||||
- `docs/DATABASE.md` - Database schema
|
||||
- `CLAUDE.md` - Common mistakes and patterns
|
||||
- `CONTRIBUTING.md` - Git workflow
|
||||
49
resources/views/components/category-tree-item.blade.php
Normal file
49
resources/views/components/category-tree-item.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@props(['category', 'type', 'business', 'level' => 0])
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
@if($level > 0)
|
||||
<span class="text-base-content/40">{{ str_repeat('—', $level) }} </span>
|
||||
@endif
|
||||
{{ $category->name }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ $type === 'product' ? ($category->products_count ?? 0) : ($category->components_count ?? 0) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, $type]) }}?parent={{ $category->id }}"
|
||||
class="btn btn-xs btn-ghost">
|
||||
<span class="icon-[lucide--plus] size-3"></span>
|
||||
Add Sub
|
||||
</a>
|
||||
<a href="{{ route('seller.business.settings.categories.edit', [$business->slug, $type, $category->id]) }}"
|
||||
class="btn btn-xs">
|
||||
<span class="icon-[lucide--pencil] size-3"></span>
|
||||
Edit
|
||||
</a>
|
||||
@php
|
||||
$hasItems = $type === 'product' ? ($category->products_count ?? 0) > 0 : ($category->components_count ?? 0) > 0;
|
||||
$hasChildren = $category->children && $category->children->count() > 0;
|
||||
@endphp
|
||||
@if(!$hasItems && !$hasChildren)
|
||||
<form method="POST"
|
||||
action="{{ route('seller.business.settings.categories.destroy', [$business->slug, $type, $category->id]) }}"
|
||||
onsubmit="return confirm('Delete this category?')"
|
||||
class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-xs btn-ghost">
|
||||
<span class="icon-[lucide--trash-2] size-3"></span>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@if($category->children && $category->children->count() > 0)
|
||||
@foreach($category->children as $child)
|
||||
<x-category-tree-item :category="$child" :type="$type" :level="$level + 1" :business="$business" />
|
||||
@endforeach
|
||||
@endif
|
||||
253
resources/views/components/seller-account-dropdown.blade.php
Normal file
253
resources/views/components/seller-account-dropdown.blade.php
Normal file
@@ -0,0 +1,253 @@
|
||||
{{-- PREVIEW COMPONENT - Seller Account Dropdown --}}
|
||||
{{-- This is a temporary preview - does not affect production navigation --}}
|
||||
|
||||
@php
|
||||
// Get business from current route parameter (for multi-business context)
|
||||
$routeBusiness = request()->route('business');
|
||||
// Fallback to primary business if no route parameter (shouldn't happen in seller context)
|
||||
$business = $routeBusiness ?? auth()->user()?->primaryBusiness();
|
||||
$user = auth()->user();
|
||||
$isOwner = $business && $business->owner_user_id === $user->id;
|
||||
$isSuperAdmin = $user->user_type === 'admin';
|
||||
$canManageCompany = $isOwner || $isSuperAdmin;
|
||||
@endphp
|
||||
|
||||
<div x-data="{ accountDropdownOpen: false }" class="relative" x-cloak>
|
||||
{{-- User Badge (Bottom Left of Sidebar) --}}
|
||||
<button
|
||||
@click="accountDropdownOpen = !accountDropdownOpen"
|
||||
class="flex items-center gap-3 w-full p-3 hover:bg-base-200 transition-colors rounded-lg"
|
||||
aria-label="Account Menu">
|
||||
{{-- Avatar --}}
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content w-10 rounded-full">
|
||||
<span class="text-sm font-semibold">
|
||||
{{ strtoupper(substr($user->first_name ?? 'U', 0, 1)) }}{{ strtoupper(substr($user->last_name ?? 'S', 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- User Info --}}
|
||||
<div class="flex-1 text-left overflow-hidden">
|
||||
<div class="text-sm font-semibold truncate">{{ trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) ?: 'User' }}</div>
|
||||
<div class="text-xs text-base-content/60 truncate">
|
||||
@if($isOwner)
|
||||
<span class="badge badge-primary badge-xs">Owner</span>
|
||||
@elseif($isSuperAdmin)
|
||||
<span class="badge badge-error badge-xs">Super Admin</span>
|
||||
@else
|
||||
<span class="text-xs">Team Member</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Chevron --}}
|
||||
<span class="icon-[lucide--chevron-up] size-4 transition-transform"
|
||||
:class="accountDropdownOpen ? 'rotate-180' : ''"></span>
|
||||
</button>
|
||||
|
||||
{{-- Dropdown Menu --}}
|
||||
<div x-show="accountDropdownOpen"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95 translate-y-2"
|
||||
x-transition:enter-end="opacity-100 scale-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
@click.away="accountDropdownOpen = false"
|
||||
class="absolute bottom-full left-0 mb-2 w-72 bg-base-100 rounded-box shadow-xl border border-base-300 z-50">
|
||||
|
||||
<div class="p-2">
|
||||
{{-- MY ACCOUNT Section (All Users) --}}
|
||||
<div class="mb-2">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
|
||||
My Account
|
||||
</div>
|
||||
<ul class="menu menu-sm">
|
||||
{{-- Profile --}}
|
||||
<li>
|
||||
<a href="{{ $business ? route('seller.business.settings.profile', $business->slug) : '#' }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--user] size-4"></span>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Notifications --}}
|
||||
<li>
|
||||
<a href="{{ $business ? route('seller.business.settings.notifications', $business->slug) : '#' }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--bell] size-4"></span>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- API Keys / Password (Owner Only) --}}
|
||||
@if($canManageCompany)
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--key] size-4"></span>
|
||||
<span>API Keys / Password</span>
|
||||
<span class="ml-auto badge badge-xs badge-warning">Coming Soon</span>
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
<li class="disabled">
|
||||
<div class="flex items-center gap-3 opacity-50 cursor-not-allowed">
|
||||
<span class="icon-[lucide--lock] size-4 text-base-content/40"></span>
|
||||
<span class="icon-[lucide--key] size-4 text-base-content/40"></span>
|
||||
<span class="text-base-content/40">API Keys / Password</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{-- MY COMPANY Section (Owner/Admin Only) --}}
|
||||
@if($canManageCompany && $business)
|
||||
<div class="divider my-1"></div>
|
||||
<div class="mb-2">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
|
||||
My Company
|
||||
</div>
|
||||
<ul class="menu menu-sm">
|
||||
{{-- Company Info --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.company-information', $business->slug) }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--building-2] size-4"></span>
|
||||
<span>Company Info</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Users & Roles --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.users', $business->slug) }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--users] size-4"></span>
|
||||
<span>Users & Roles</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Licenses / Compliance --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.manage-licenses', $business->slug) }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--file-check] size-4"></span>
|
||||
<span>Licenses / Compliance</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Payments & Subscriptions --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.plans-and-billing', $business->slug) }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--credit-card] size-4"></span>
|
||||
<span>Payments & Subscriptions</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Integrations --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.integrations', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--plug] size-4"></span>
|
||||
<span>Integrations</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ADMIN CONSOLE Section (Owner/Admin Only) --}}
|
||||
@if($canManageCompany && $business)
|
||||
<div class="divider my-1"></div>
|
||||
<div class="mb-2">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
|
||||
Admin Console
|
||||
</div>
|
||||
<ul class="menu menu-sm">
|
||||
{{-- Sales Config --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.sales-config', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--shopping-cart] size-4"></span>
|
||||
<span>Sales Config</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Inventory/Manufacturing Config --}}
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--factory] size-4"></span>
|
||||
<span>Manufacturing Config</span>
|
||||
<span class="ml-auto badge badge-xs badge-warning">Coming Soon</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Categories --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--folder-tree] size-4"></span>
|
||||
<span>Categories</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Messaging Providers --}}
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--mail] size-4"></span>
|
||||
<span>Messaging Providers</span>
|
||||
<span class="ml-auto badge badge-xs badge-warning">Coming Soon</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Brand Kits & Assets --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.brand-kit', $business->slug) }}"
|
||||
class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--palette] size-4"></span>
|
||||
<span>Brand Kits & Assets</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Webhooks / API --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.webhooks', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--webhook] size-4"></span>
|
||||
<span>Webhooks / API</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Audit Logs --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.audit-logs', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--file-text] size-4"></span>
|
||||
<span>Audit Logs</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{{-- Reports --}}
|
||||
<li>
|
||||
<a href="{{ route('seller.business.settings.reports', $business->slug) }}" class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--bar-chart] size-4"></span>
|
||||
<span>Reports</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Sign Out --}}
|
||||
<div class="divider my-1"></div>
|
||||
<ul class="menu menu-sm">
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="flex items-center gap-3 w-full text-error">
|
||||
<span class="icon-[lucide--log-out] size-4"></span>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,9 +354,13 @@
|
||||
<div class="mb-2">
|
||||
<hr class="border-base-300 my-2 border-dashed" />
|
||||
|
||||
<div class="px-2 mb-3">
|
||||
<x-seller-account-dropdown />
|
||||
</div>
|
||||
|
||||
<!-- Version Info Section -->
|
||||
<div class="mx-2 mb-3 px-3 text-xs text-center text-base-content/50">
|
||||
<p class="mb-0.5">{{ config('version.company.name') }} hub</p>
|
||||
<div class="mx-2 px-3 text-xs text-center text-base-content/50">
|
||||
<p class="mb-0.5">Made with ❤️ by Creationshop</p>
|
||||
<p class="font-mono mb-0.5">
|
||||
@if($appVersion === 'dev')
|
||||
<span class="text-yellow-500 font-semibold">DEV</span> sha-{{ $appCommit }}
|
||||
@@ -364,54 +368,7 @@
|
||||
v{{ $appVersion }} (sha-{{ $appCommit }})
|
||||
@endif
|
||||
</p>
|
||||
<p>© {{ date('Y') }} {{ config('version.company.name') }}.com, {{ config('version.company.suffix') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-top dropdown-end w-full">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="bg-base-200 hover:bg-base-300 rounded-box mx-2 mt-0 flex cursor-pointer items-center gap-2.5 px-3 py-2 transition-all">
|
||||
<div class="avatar">
|
||||
<div class="bg-base-200 mask mask-squircle w-8">
|
||||
<img src="https://ui-avatars.com/api/?name={{ urlencode(trim((auth()->user()->first_name ?? '') . ' ' . (auth()->user()->last_name ?? '')) ?: 'User') }}&color=7F9CF5&background=EBF4FF" alt="Avatar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow -space-y-0.5">
|
||||
<p class="text-sm font-medium">{{ trim((auth()->user()->first_name ?? '') . ' ' . (auth()->user()->last_name ?? '')) ?: 'User' }}</p>
|
||||
<p class="text-base-content/60 text-xs">{{ auth()->user()->email ?? '' }}</p>
|
||||
</div>
|
||||
<span class="icon-[lucide--chevrons-up-down] text-base-content/60 size-4"></span>
|
||||
</div>
|
||||
<ul
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box shadow-base-content/20 mb-1 w-48 p-1 shadow-lg">
|
||||
<li>
|
||||
<a href="{{ route('seller.settings') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span>Account Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('seller.notifications.index') }}">
|
||||
<span class="icon-[lucide--bell] size-4"></span>
|
||||
<span>Notifications</span>
|
||||
</a>
|
||||
</li>
|
||||
<div class="divider my-0"></div>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-full text-left">
|
||||
<span class="icon-[lucide--log-out] size-4"></span>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<p>© {{ date('Y') }} Creationshop LLC</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
{{-- Top Header Bar --}}
|
||||
<div class="bg-white border border-gray-300 rounded-md shadow-sm mb-6">
|
||||
<div class="px-6 py-4">
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between gap-6">
|
||||
{{-- Left: Product Image & Info --}}
|
||||
<div class="flex items-start gap-4">
|
||||
@@ -13,10 +13,10 @@
|
||||
@if($product->images->where('is_primary', true)->first())
|
||||
<img src="{{ asset('storage/' . $product->images->where('is_primary', true)->first()->path) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-16 h-16 object-cover rounded-md border border-gray-300">
|
||||
class="w-16 h-16 object-cover rounded-md border border-base-300">
|
||||
@else
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-md border border-gray-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="w-16 h-16 bg-base-200 rounded-md border border-base-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
{{-- Product Details --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h1 class="text-xl font-bold text-gray-900">{{ $product->name }}</h1>
|
||||
<h1 class="text-xl font-bold text-base-content">{{ $product->name }}</h1>
|
||||
{{-- Status Badges --}}
|
||||
<span id="activeBadge" style="display: {{ $product->is_active ? 'inline-flex' : 'none' }};" class="items-center px-2.5 py-0.5 rounded text-xs font-medium bg-success text-white">
|
||||
Active
|
||||
@@ -35,39 +35,30 @@
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-0.5">
|
||||
<div class="text-sm text-base-content/70 space-y-0.5">
|
||||
<div><span class="font-medium">SKU:</span> <span class="font-mono">{{ $product->sku ?? 'N/A' }}</span> <span class="mx-2">•</span> <span class="font-medium">Brand:</span> {{ $product->brand->name ?? 'N/A' }}</div>
|
||||
<div><span class="font-medium">Last updated:</span> {{ $product->updated_at->format('M j, Y g:i A') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Right: Action Buttons & Breadcrumb --}}
|
||||
{{-- Right: Action Buttons --}}
|
||||
<div class="flex flex-col gap-2 flex-shrink-0">
|
||||
{{-- View on Marketplace Button (White with border) --}}
|
||||
<a href="#" target="_blank" class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- View on Marketplace Button --}}
|
||||
<a href="#" target="_blank" class="btn btn-outline btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
View on Marketplace
|
||||
</a>
|
||||
|
||||
{{-- Manage BOM Button (Blue solid) --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-primary hover:bg-primary/90 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- Manage BOM Button --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="btn btn-primary btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||
</svg>
|
||||
Manage BOM
|
||||
</a>
|
||||
|
||||
{{-- Breadcrumb Navigation --}}
|
||||
<nav class="flex text-xs text-gray-500 mt-1" aria-label="Breadcrumb">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="hover:text-gray-700">Dashboard</a>
|
||||
<span class="mx-2">></span>
|
||||
<a href="{{ route('seller.business.products.index', $business->slug) }}" class="hover:text-gray-700">Products</a>
|
||||
<span class="mx-2">></span>
|
||||
<span class="text-gray-900 font-medium">Edit</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +95,7 @@
|
||||
{{-- LEFT SIDEBAR (1/4 width) --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Product Images Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Product Images</h2>
|
||||
<div class="space-y-4">
|
||||
@@ -147,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Quick Stats Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Quick Stats</h2>
|
||||
<div class="space-y-2">
|
||||
@@ -173,7 +164,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Audit Info Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xs">Audit Info</h2>
|
||||
<div class="space-y-1 text-xs text-base-content/70">
|
||||
@@ -186,7 +177,7 @@
|
||||
|
||||
{{-- MAIN CONTENT WITH TABS (3/4 width) --}}
|
||||
<div class="lg:col-span-3">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{{-- Tabs Navigation --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
236
resources/views/seller/settings/audit-logs.blade.php
Normal file
236
resources/views/seller/settings/audit-logs.blade.php
Normal file
@@ -0,0 +1,236 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--scroll-text] size-6"></span>
|
||||
<p class="text-lg font-medium">Audit Logs</p>
|
||||
</div>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-60">Audit Logs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Audit Logs Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--scroll-text] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">System Activity</h2>
|
||||
</div>
|
||||
|
||||
<!-- Filter and Search Controls -->
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-outline gap-2">
|
||||
<span class="icon-[lucide--filter] size-4"></span>
|
||||
Add Filter
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="mb-4">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="icon-[lucide--search] size-4 text-base-content/40"></span>
|
||||
<input type="text" class="grow" placeholder="Search audit logs..." id="auditSearch" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Audit Logs Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center gap-1 cursor-pointer hover:text-primary">
|
||||
Timestamp
|
||||
<span class="icon-[lucide--arrow-up-down] size-3"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Resource</th>
|
||||
<th>IP Address</th>
|
||||
<th>User Agent</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($audits as $audit)
|
||||
<tr class="hover">
|
||||
<td class="text-sm">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ $audit->created_at->format('M d, Y') }}</span>
|
||||
<span class="text-xs text-base-content/60">{{ $audit->created_at->format('h:i:s A') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-sm">{{ $audit->user?->name ?? 'System' }}</span>
|
||||
<span class="text-xs text-base-content/60">{{ $audit->user?->email ?? 'system@automated' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($audit->event === 'created')
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[lucide--plus-circle] size-3"></span>
|
||||
Created
|
||||
</span>
|
||||
@elseif($audit->event === 'updated')
|
||||
<span class="badge badge-info badge-sm gap-1">
|
||||
<span class="icon-[lucide--pencil] size-3"></span>
|
||||
Updated
|
||||
</span>
|
||||
@elseif($audit->event === 'deleted')
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--trash-2] size-3"></span>
|
||||
Deleted
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-ghost badge-sm">{{ ucfirst($audit->event) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-sm">{{ class_basename($audit->auditable_type) }}</span>
|
||||
<span class="text-xs text-base-content/60">#{{ $audit->auditable_id }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="font-mono text-xs">{{ $audit->ip_address ?? 'N/A' }}</td>
|
||||
<td class="max-w-xs">
|
||||
<div class="tooltip tooltip-left" data-tip="{{ $audit->user_agent ?? 'N/A' }}">
|
||||
<span class="text-xs text-base-content/60 truncate block">
|
||||
{{ Str::limit($audit->user_agent ?? 'N/A', 30) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="viewAuditDetails{{ $audit->id }}.showModal()" class="btn btn-ghost btn-sm btn-square">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Audit Details Modal -->
|
||||
<dialog id="viewAuditDetails{{ $audit->id }}" class="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<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 mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--file-text] size-5"></span>
|
||||
Audit Details
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">Timestamp</label>
|
||||
<p class="text-sm">{{ $audit->created_at->format('F d, Y \a\t h:i:s A') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">Action</label>
|
||||
<p class="text-sm">{{ ucfirst($audit->event) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">User</label>
|
||||
<p class="text-sm">{{ $audit->user?->name ?? 'System' }}</p>
|
||||
<p class="text-xs text-base-content/60">{{ $audit->user?->email ?? 'system@automated' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">IP Address</label>
|
||||
<p class="text-sm font-mono">{{ $audit->ip_address ?? 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-semibold text-base-content/60">Resource</label>
|
||||
<p class="text-sm">{{ class_basename($audit->auditable_type) }} #{{ $audit->auditable_id }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-semibold text-base-content/60">User Agent</label>
|
||||
<p class="text-xs font-mono break-all">{{ $audit->user_agent ?? 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Changes -->
|
||||
@if($audit->old_values || $audit->new_values)
|
||||
<div class="divider"></div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60 mb-2 block">Changes</label>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_keys(array_merge($audit->old_values ?? [], $audit->new_values ?? [])) as $field)
|
||||
<div class="bg-base-200 p-3 rounded-lg">
|
||||
<div class="font-semibold text-sm mb-1">{{ ucfirst($field) }}</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span class="text-base-content/60">Old Value:</span>
|
||||
<div class="bg-error/10 text-error p-2 rounded mt-1 font-mono">{{ $audit->old_values[$field] ?? 'N/A' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">New Value:</span>
|
||||
<div class="bg-success/10 text-success p-2 rounded mt-1 font-mono">{{ $audit->new_values[$field] ?? 'N/A' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-8 text-base-content/60">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="icon-[lucide--inbox] size-12 text-base-content/20"></span>
|
||||
<p>No audit logs found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($audits->hasPages())
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div class="text-sm text-base-content/60">
|
||||
Showing {{ $audits->firstItem() ?? 0 }} to {{ $audits->lastItem() ?? 0 }} of {{ $audits->total() }} entries
|
||||
</div>
|
||||
<div>
|
||||
{{ $audits->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple client-side search functionality
|
||||
document.getElementById('auditSearch').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
336
resources/views/seller/settings/brand-kit.blade.php
Normal file
336
resources/views/seller/settings/brand-kit.blade.php
Normal file
@@ -0,0 +1,336 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Brand Kit</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.company-information', $business->slug) }}">Company</a></li>
|
||||
<li class="opacity-80">Brand Kit</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header with Actions -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content flex items-center gap-2">
|
||||
<span class="icon-[lucide--palette] size-8"></span>
|
||||
Cannabrands Brand Kit
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Download brand assets for Cannabrands</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-outline btn-sm gap-2">
|
||||
<span class="icon-[lucide--upload] size-4"></span>
|
||||
Upload
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Logos Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--image] size-5 text-primary"></span>
|
||||
Logos
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands logos in various formats
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Primary Logo SVG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-white rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-primary.svg') }}"
|
||||
alt="Cannabrands Primary Logo"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsLogo%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Primary Logo</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">SVG Format</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-primary.svg') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Logo PNG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-white rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-primary.png') }}"
|
||||
alt="Cannabrands Primary Logo PNG"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsLogo%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Primary Logo</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">PNG Format (High-Res)</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-primary.png') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Logo SVG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-base-300 rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-secondary.svg') }}"
|
||||
alt="Cannabrands Secondary Logo"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsIcon%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Icon/Mark</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">SVG Format</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-secondary.svg') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colors Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--palette] size-5 text-primary"></span>
|
||||
Colors
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands brand colors with hex codes
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@php
|
||||
$colors = [
|
||||
['name' => 'Primary Green', 'hex' => '#10B981', 'description' => 'Main brand color'],
|
||||
['name' => 'Dark Green', 'hex' => '#047857', 'description' => 'Secondary'],
|
||||
['name' => 'Light Green', 'hex' => '#D1FAE5', 'description' => 'Backgrounds'],
|
||||
['name' => 'Charcoal', 'hex' => '#1F2937', 'description' => 'Text primary'],
|
||||
['name' => 'Gray', 'hex' => '#6B7280', 'description' => 'Text secondary'],
|
||||
['name' => 'White', 'hex' => '#FFFFFF', 'description' => 'Backgrounds'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($colors as $color)
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-3">
|
||||
<div class="rounded-lg mb-2 h-20 border border-base-300"
|
||||
style="background-color: {{ $color['hex'] }};">
|
||||
</div>
|
||||
<h3 class="font-medium text-xs">{{ $color['name'] }}</h3>
|
||||
<p class="text-xs font-mono text-base-content/80">{{ $color['hex'] }}</p>
|
||||
<p class="text-xs text-base-content/50">{{ $color['description'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/colors.json') }}"
|
||||
download
|
||||
class="btn btn-sm btn-primary gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download Color Palette (JSON)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typography Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--type] size-5 text-primary"></span>
|
||||
Typography
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands font files and usage guidelines
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Headings Font -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">Inter Bold</h3>
|
||||
<p class="text-sm text-base-content/60">Headings & Display Text</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Bold.ttf') }}"
|
||||
download
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
TTF
|
||||
</a>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Bold.woff') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
WOFF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold">The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Font -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg">Inter Regular</h3>
|
||||
<p class="text-sm text-base-content/60">Body Text & Paragraphs</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Regular.ttf') }}"
|
||||
download
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
TTF
|
||||
</a>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Regular.woff') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
WOFF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base">The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--layout-template] size-5 text-primary"></span>
|
||||
Templates
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Ready-to-use templates for marketing materials
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Email Template -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="rounded-lg bg-primary/10 p-3">
|
||||
<span class="icon-[lucide--mail] size-6 text-primary"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-medium">Email Template</h3>
|
||||
<p class="text-xs text-base-content/60">HTML email template</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
Preview
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/templates/email-template.html') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Template -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="rounded-lg bg-primary/10 p-3">
|
||||
<span class="icon-[lucide--share-2] size-6 text-primary"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-medium">Social Media</h3>
|
||||
<p class="text-xs text-base-content/60">Instagram & Facebook posts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
Preview
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/templates/social-media.zip') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Guidelines Note -->
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div>
|
||||
<h3 class="font-bold">Brand Guidelines</h3>
|
||||
<div class="text-sm">
|
||||
Please follow Cannabrands brand guidelines when using these assets.
|
||||
For questions or custom requests, contact the marketing team.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Preview modal functionality can be added here
|
||||
console.log('Brand Kit page loaded');
|
||||
</script>
|
||||
@endpush
|
||||
136
resources/views/seller/settings/categories/create.blade.php
Normal file
136
resources/views/seller/settings/categories/create.blade.php
Normal file
@@ -0,0 +1,136 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Create {{ ucfirst($type) }} Category</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.categories.index', $business->slug) }}">Categories</a></li>
|
||||
<li class="opacity-80">Create</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Create {{ ucfirst($type) }} Category</h1>
|
||||
<p class="text-base-content/60 mt-1">Add a new {{ $type }} category for organizing your {{ $type }}s</p>
|
||||
</div>
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--arrow-left] size-4"></span>
|
||||
Back to Categories
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Create Form -->
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<form method="POST" action="{{ route('seller.business.settings.categories.store', [$business->slug, $type]) }}" enctype="multipart/form-data" class="card-body">
|
||||
@csrf
|
||||
|
||||
<!-- Category Image -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Category Image</span>
|
||||
</label>
|
||||
<input type="file" name="image" accept="image/*" class="file-input file-input-bordered w-full" />
|
||||
@error('image')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" class="input input-bordered w-full" required autofocus />
|
||||
@error('name')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Parent Category -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Parent</span>
|
||||
</label>
|
||||
<select name="parent_id" class="select select-bordered w-full">
|
||||
<option value="">Top-level Category</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" {{ old('parent_id', request('parent')) == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
</option>
|
||||
@if($cat->children && $cat->children->count() > 0)
|
||||
@foreach($cat->children as $child)
|
||||
<option value="{{ $child->id }}" {{ old('parent_id', request('parent')) == $child->id ? 'selected' : '' }}>
|
||||
— {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Leave blank to create a top-level category</span>
|
||||
</label>
|
||||
@error('parent_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea name="description" rows="4" class="textarea textarea-bordered w-full">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Sort Order</span>
|
||||
</label>
|
||||
<input type="number" name="sort_order" value="{{ old('sort_order', 0) }}" min="0" class="input input-bordered w-full" />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Lower numbers appear first</span>
|
||||
</label>
|
||||
@error('sort_order')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Active Status</span>
|
||||
</label>
|
||||
<div>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="is_active" value="1" class="checkbox" {{ old('is_active', true) ? 'checked' : '' }} />
|
||||
<span class="label-text">Active</span>
|
||||
</label>
|
||||
<label class="label pt-0">
|
||||
<span class="label-text-alt">Inactive categories are hidden from selection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Category
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
151
resources/views/seller/settings/categories/edit.blade.php
Normal file
151
resources/views/seller/settings/categories/edit.blade.php
Normal file
@@ -0,0 +1,151 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Edit {{ ucfirst($type) }} Category</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.categories.index', $business->slug) }}">Categories</a></li>
|
||||
<li class="opacity-80">Edit</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Edit {{ ucfirst($type) }} Category</h1>
|
||||
<p class="text-base-content/60 mt-1">Update category information</p>
|
||||
</div>
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--arrow-left] size-4"></span>
|
||||
Back to Categories
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<form method="POST" action="{{ route('seller.business.settings.categories.update', [$business->slug, $type, $category->id]) }}" enctype="multipart/form-data" class="card-body">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Current Image -->
|
||||
@if($category->image_path)
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Image</span>
|
||||
</label>
|
||||
<div class="avatar">
|
||||
<div class="w-24 rounded">
|
||||
<img src="{{ asset('storage/' . $category->image_path) }}" alt="{{ $category->name }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Category Image -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ $category->image_path ? 'Replace' : 'Upload' }} Category Image</span>
|
||||
</label>
|
||||
<input type="file" name="image" accept="image/*" class="file-input file-input-bordered w-full" />
|
||||
@error('image')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $category->name) }}" class="input input-bordered w-full" required autofocus />
|
||||
@error('name')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Parent Category -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Parent</span>
|
||||
</label>
|
||||
<select name="parent_id" class="select select-bordered w-full">
|
||||
<option value="">Top-level Category</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" {{ old('parent_id', $category->parent_id) == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
</option>
|
||||
@if($cat->children && $cat->children->count() > 0)
|
||||
@foreach($cat->children as $child)
|
||||
<option value="{{ $child->id }}" {{ old('parent_id', $category->parent_id) == $child->id ? 'selected' : '' }}>
|
||||
— {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Leave blank for a top-level category</span>
|
||||
</label>
|
||||
@error('parent_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea name="description" rows="4" class="textarea textarea-bordered w-full">{{ old('description', $category->description) }}</textarea>
|
||||
@error('description')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Sort Order</span>
|
||||
</label>
|
||||
<input type="number" name="sort_order" value="{{ old('sort_order', $category->sort_order ?? 0) }}" min="0" class="input input-bordered w-full" />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Lower numbers appear first</span>
|
||||
</label>
|
||||
@error('sort_order')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Active Status</span>
|
||||
</label>
|
||||
<div>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="is_active" value="1" class="checkbox" {{ old('is_active', $category->is_active) ? 'checked' : '' }} />
|
||||
<span class="label-text">Active</span>
|
||||
</label>
|
||||
<label class="label pt-0">
|
||||
<span class="label-text-alt">Inactive categories are hidden from selection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Update Category
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
142
resources/views/seller/settings/categories/index.blade.php
Normal file
142
resources/views/seller/settings/categories/index.blade.php
Normal file
@@ -0,0 +1,142 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Categories</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-80">Categories</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content flex items-center gap-2">
|
||||
<span class="icon-[lucide--folder-tree] size-8"></span>
|
||||
Categories
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Product and component categories for {{ $business->name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
@if(session('success'))
|
||||
<div class="alert mb-6">
|
||||
<span class="icon-[lucide--check-circle] size-5"></span>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert mb-6">
|
||||
<span class="icon-[lucide--alert-circle] size-5"></span>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Categories Tables -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Product Categories -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<span class="icon-[lucide--package] size-6"></span>
|
||||
Product Categories
|
||||
</h2>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'product']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Category
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($productCategories->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<span class="icon-[lucide--folder-x] size-12 text-base-content/20 mx-auto block mb-2"></span>
|
||||
<p class="text-base-content/60 mb-4">No product categories found</p>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'product']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Create First Category
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-right">Products</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($productCategories as $category)
|
||||
<x-category-tree-item :category="$category" type="product" :business="$business" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-base-content/60">
|
||||
Total: {{ $productCategories->count() }} top-level categories
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Component Categories -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<span class="icon-[lucide--box] size-6"></span>
|
||||
Component Categories
|
||||
</h2>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'component']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Category
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($componentCategories->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<span class="icon-[lucide--folder-x] size-12 text-base-content/20 mx-auto block mb-2"></span>
|
||||
<p class="text-base-content/60 mb-4">No component categories found</p>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'component']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Create First Category
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-right">Components</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($componentCategories as $category)
|
||||
<x-category-tree-item :category="$category" type="component" :business="$business" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-base-content/60">
|
||||
Total: {{ $componentCategories->count() }} top-level categories
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -2,27 +2,387 @@
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Company Information</p>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Company Information</h1>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Company Information</li>
|
||||
<li><a href="{{ route('seller.business.settings.company-information', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">Company Information</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Company Information Settings</h2>
|
||||
<p class="text-base-content/60">Manage your company details, DBA, address, and other information.</p>
|
||||
<form action="{{ route('seller.business.settings.company-information.update', $business->slug) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Content (Left Side - 2 columns) -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Company Overview -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Company Overview</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Company Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $business->name) }}"
|
||||
class="input input-bordered @error('name') input-error @enderror"
|
||||
placeholder="Your company name" required>
|
||||
@error('name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- DBA Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">DBA Name</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="'Doing Business As' name if different from legal name">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" name="dba_name" value="{{ old('dba_name', $business->dba_name) }}"
|
||||
class="input input-bordered @error('dba_name') input-error @enderror"
|
||||
placeholder="Trade name or DBA">
|
||||
@error('dba_name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description (Full Width - Spans 2 columns) -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Description</span>
|
||||
<span class="label-text-alt flex items-center gap-2">
|
||||
<span class="tooltip tooltip-left" data-tip="This appears in the About Company modal">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
<span class="text-sm" id="description-count">0/500</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea name="description"
|
||||
rows="4"
|
||||
class="textarea textarea-bordered w-full resize-none @error('description') textarea-error @enderror"
|
||||
placeholder="Describe your company, mission, and what makes you unique"
|
||||
maxlength="500"
|
||||
oninput="updateCharCount('description', 'description-count', 500)">{{ old('description', $business->description) }}</textarea>
|
||||
@error('description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company Branding -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Company Branding</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Company Logo -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Logo</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Displayed in About Company modal">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
@if($business->logo_path && \Storage::disk('public')->exists($business->logo_path))
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $business->logo_path) }}" alt="Company logo" class="w-32 h-32 rounded-lg border border-base-300 object-contain bg-base-50 p-2">
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="logo" accept="image/*"
|
||||
class="file-input file-input-bordered @error('logo') file-input-error @enderror"
|
||||
onchange="previewImage(this, 'logo-preview')">
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Max 2MB. Recommended: Square (512x512px)</span>
|
||||
</label>
|
||||
@error('logo')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
<div id="logo-preview" class="mt-2 hidden">
|
||||
<p class="text-sm text-base-content/60 mb-1">Preview:</p>
|
||||
<img src="" alt="Logo preview" class="w-32 h-32 rounded-lg border border-base-300 object-contain bg-base-50 p-2">
|
||||
</div>
|
||||
@if($business->logo_path && \Storage::disk('public')->exists($business->logo_path))
|
||||
<label class="label cursor-pointer justify-start gap-2 mt-2">
|
||||
<input type="checkbox" name="remove_logo" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current logo</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Company Banner -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Banner</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Displayed as hero banner on brand preview pages">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
@if($business->banner_path && \Storage::disk('public')->exists($business->banner_path))
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $business->banner_path) }}" alt="Company banner" class="w-full h-24 rounded-lg border border-base-300 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="banner" accept="image/*"
|
||||
class="file-input file-input-bordered @error('banner') file-input-error @enderror"
|
||||
onchange="previewImage(this, 'banner-preview')">
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Max 4MB. Recommended: 1920x640px (3:1 ratio)</span>
|
||||
</label>
|
||||
@error('banner')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
<div id="banner-preview" class="mt-2 hidden">
|
||||
<p class="text-sm text-base-content/60 mb-1">Preview:</p>
|
||||
<img src="" alt="Banner preview" class="w-full h-24 rounded-lg border border-base-300 object-cover">
|
||||
</div>
|
||||
@if($business->banner_path && \Storage::disk('public')->exists($business->banner_path))
|
||||
<label class="label cursor-pointer justify-start gap-2 mt-2">
|
||||
<input type="checkbox" name="remove_banner" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current banner</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Contact Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Business Phone -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Business Phone</span>
|
||||
</label>
|
||||
<input type="tel" name="business_phone" value="{{ old('business_phone', $business->business_phone) }}"
|
||||
class="input input-bordered @error('business_phone') input-error @enderror"
|
||||
placeholder="(555) 123-4567">
|
||||
@error('business_phone')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Business Email -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Business Email</span>
|
||||
</label>
|
||||
<input type="email" name="business_email" value="{{ old('business_email', $business->business_email) }}"
|
||||
class="input input-bordered @error('business_email') input-error @enderror"
|
||||
placeholder="info@company.com">
|
||||
@error('business_email')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Physical Address -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Physical Address</h2>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Street Address -->
|
||||
<div class="form-control col-span-12 md:col-span-8">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<input type="text" name="physical_address" value="{{ old('physical_address', $business->physical_address) }}"
|
||||
class="input input-bordered w-full @error('physical_address') input-error @enderror"
|
||||
placeholder="123 Main Street">
|
||||
@error('physical_address')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Suite/Unit Number -->
|
||||
<div class="form-control col-span-12 md:col-span-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Suite/Unit</span>
|
||||
</label>
|
||||
<input type="text" name="physical_suite" value="{{ old('physical_suite', $business->physical_suite) }}"
|
||||
class="input input-bordered w-full @error('physical_suite') input-error @enderror"
|
||||
placeholder="Suite 100">
|
||||
@error('physical_suite')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- City -->
|
||||
<div class="form-control col-span-12 md:col-span-6">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input type="text" name="physical_city" value="{{ old('physical_city', $business->physical_city) }}"
|
||||
class="input input-bordered w-full @error('physical_city') input-error @enderror"
|
||||
placeholder="Phoenix">
|
||||
@error('physical_city')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- State -->
|
||||
<div class="form-control col-span-12 md:col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">State</span>
|
||||
</label>
|
||||
<input type="text" name="physical_state" value="{{ old('physical_state', $business->physical_state) }}"
|
||||
class="input input-bordered w-full @error('physical_state') input-error @enderror"
|
||||
placeholder="AZ"
|
||||
maxlength="2">
|
||||
@error('physical_state')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- ZIP Code -->
|
||||
<div class="form-control col-span-12 md:col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ZIP Code</span>
|
||||
</label>
|
||||
<input type="text" name="physical_zipcode" value="{{ old('physical_zipcode', $business->physical_zipcode) }}"
|
||||
class="input input-bordered w-full @error('physical_zipcode') input-error @enderror"
|
||||
placeholder="85001">
|
||||
@error('physical_zipcode')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar (1 column) -->
|
||||
<div class="lg:col-span-1 space-y-6">
|
||||
<!-- License Information -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">License Information</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- License Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">License Number</span>
|
||||
</label>
|
||||
<input type="text" name="license_number" value="{{ old('license_number', $business->license_number) }}"
|
||||
class="input input-bordered @error('license_number') input-error @enderror"
|
||||
placeholder="AZ-MED-00001234">
|
||||
@error('license_number')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- License Type -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">License Type</span>
|
||||
</label>
|
||||
<select name="license_type" class="select select-bordered @error('license_type') select-error @enderror">
|
||||
<option value="">Select license type</option>
|
||||
<option value="medical" {{ old('license_type', $business->license_type) == 'medical' ? 'selected' : '' }}>Medical</option>
|
||||
<option value="adult-use" {{ old('license_type', $business->license_type) == 'adult-use' ? 'selected' : '' }}>Adult Use</option>
|
||||
<option value="both" {{ old('license_type', $business->license_type) == 'both' ? 'selected' : '' }}>Both</option>
|
||||
</select>
|
||||
@error('license_type')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost gap-2">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Character counter
|
||||
function updateCharCount(textareaName, counterId, maxLength) {
|
||||
const textarea = document.querySelector(`[name="${textareaName}"]`);
|
||||
const counter = document.getElementById(counterId);
|
||||
if (textarea && counter) {
|
||||
counter.textContent = `${textarea.value.length}/${maxLength}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Image preview
|
||||
function previewImage(input, previewId) {
|
||||
const preview = document.getElementById(previewId);
|
||||
const img = preview.querySelector('img');
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
img.src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize character counters on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateCharCount('description', 'description-count', 500);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
302
resources/views/seller/settings/integrations.blade.php
Normal file
302
resources/views/seller/settings/integrations.blade.php
Normal file
@@ -0,0 +1,302 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-gradient-to-br from-slate-50 to-slate-100 py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header Section -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Integrations</h1>
|
||||
</div>
|
||||
<p class="text-gray-600 text-lg">Connect Cannabrands with your favorite apps to streamline your workflow</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="mb-8 flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search integrations..."
|
||||
class="input input-bordered w-full pl-10 bg-white shadow-sm"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<select class="select select-bordered bg-white shadow-sm w-full sm:w-48">
|
||||
<option disabled selected>Category</option>
|
||||
<option>All Categories</option>
|
||||
<option>Accounting</option>
|
||||
<option>Time Tracking</option>
|
||||
<option>Expense Management</option>
|
||||
<option>Financial Services</option>
|
||||
<option>Analytics</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Integrations Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
<!-- Integration Card 1: Dext Prepare -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Dext Prepare</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Automated receipt and invoice capture for seamless expense tracking</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.8</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(247 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 2: Bill.com -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Bill.com</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Streamline bill payments and receivables management</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.7</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(193 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 3: QuickBooks Time PREMIUM -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-bold text-gray-900">QuickBooks Time</h3>
|
||||
<span class="badge badge-sm badge-accent font-bold text-xs">PREMIUM</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Track employee hours and streamline payroll</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.6</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-gray-300 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(156 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 4: Fundbox -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Fundbox</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Fast business lines of credit when you need it</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.5</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(128 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 5: Expensify PREMIUM -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-indigo-400 to-indigo-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-bold text-gray-900">Expensify</h3>
|
||||
<span class="badge badge-sm badge-accent font-bold text-xs">PREMIUM</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Automate expense reporting and receipts management</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.9</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(312 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 6: Fathom -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Fathom Analytics</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Get detailed insights into your business performance</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.4</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-gray-300 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(89 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Load More Button -->
|
||||
<div class="flex justify-center">
|
||||
<button class="btn btn-outline btn-lg gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Load More Integrations
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Smooth hover effects for cards */
|
||||
.card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* Star rating animation */
|
||||
svg.fill-current {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge-accent {
|
||||
background-color: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Smooth transitions on buttons */
|
||||
.btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Icon backgrounds with gradient */
|
||||
.bg-gradient-to-br {
|
||||
background-image: linear-gradient(135deg, var(--tw-gradient-stops));
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -3,24 +3,349 @@
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Manage Licenses</p>
|
||||
<p class="text-lg font-medium">Licenses & Compliance</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Manage Licenses</li>
|
||||
<li class="opacity-80">Licenses</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section -->
|
||||
@if($expiredLocations->count() > 0 || $expiringLocations->count() > 0)
|
||||
<div class="mt-6 space-y-3">
|
||||
@if($expiredLocations->count() > 0)
|
||||
<div role="alert" class="alert alert-error">
|
||||
<span class="icon-[lucide--alert-circle] size-5"></span>
|
||||
<div>
|
||||
<h3 class="font-bold">Expired Licenses</h3>
|
||||
<div class="text-sm">
|
||||
{{ $expiredLocations->count() }} {{ Str::plural('location', $expiredLocations->count()) }} {{ $expiredLocations->count() === 1 ? 'has an' : 'have' }} expired license. Immediate action required.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($expiringLocations->count() > 0)
|
||||
<div role="alert" class="alert alert-warning">
|
||||
<span class="icon-[lucide--alert-triangle] size-5"></span>
|
||||
<div>
|
||||
<h3 class="font-bold">Licenses Expiring Soon</h3>
|
||||
<div class="text-sm">
|
||||
{{ $expiringLocations->count() }} {{ Str::plural('license', $expiringLocations->count()) }} will expire within 30 days. Renewal required.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Business-Level License Info -->
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">License Management</h2>
|
||||
<p class="text-base-content/60">Manage business licenses and compliance documents.</p>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--building-2] size-5 text-primary"></span>
|
||||
Primary Business License
|
||||
</h2>
|
||||
<a href="{{ route('seller.business.settings.company-information', $business->slug) }}"
|
||||
class="btn btn-sm btn-ghost">
|
||||
<span class="icon-[lucide--pencil] size-4"></span>
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<div class="text-sm text-base-content/60 mb-1">License Number</div>
|
||||
<div class="font-medium">{{ $business->license_number ?? 'Not set' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-base-content/60 mb-1">License Type</div>
|
||||
<div class="font-medium">
|
||||
@if($business->license_type)
|
||||
<span class="badge badge-primary">{{ ucfirst(str_replace('_', ' ', $business->license_type)) }}</span>
|
||||
@else
|
||||
Not set
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-sm text-base-content/60 mb-1">Tax Exempt</div>
|
||||
<div class="font-medium">
|
||||
@if($business->tax_exempt)
|
||||
<span class="badge badge-success gap-2">
|
||||
<span class="icon-[lucide--check] size-3"></span>
|
||||
Yes
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-ghost">No</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($business->tax_exemption_reason)
|
||||
<div class="mt-4">
|
||||
<div class="text-sm text-base-content/60 mb-1">Tax Exemption Reason</div>
|
||||
<div class="text-sm">{{ $business->tax_exemption_reason }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Licenses -->
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--map-pin] size-5 text-primary"></span>
|
||||
Location Licenses
|
||||
</h2>
|
||||
@if($business->locations()->count() === 0)
|
||||
<a href="{{ route('seller.business.locations.create', $business->slug) }}"
|
||||
class="btn btn-sm btn-primary">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Location
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($locations->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>License Number</th>
|
||||
<th>License Type</th>
|
||||
<th>Status</th>
|
||||
<th>Expiration</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($locations as $location)
|
||||
@php
|
||||
$isExpired = $location->license_expiration && $location->license_expiration->isPast();
|
||||
$isExpiringSoon = $location->license_expiration &&
|
||||
$location->license_expiration->isFuture() &&
|
||||
$location->license_expiration->diffInDays(now()) <= 30;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-medium">{{ $location->name }}</div>
|
||||
<div class="text-sm text-base-content/60">
|
||||
{{ $location->address }}, {{ $location->city }}, {{ $location->state }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-sm">{{ $location->license_number }}</code>
|
||||
</td>
|
||||
<td>
|
||||
@if($location->license_type)
|
||||
<span class="badge badge-sm">
|
||||
{{ \App\Models\Location::LICENSE_TYPES[$location->license_type] ?? $location->license_type }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-base-content/40">Not set</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($location->license_status === 'active')
|
||||
<span class="badge badge-success badge-sm gap-2">
|
||||
<span class="icon-[lucide--check-circle] size-3"></span>
|
||||
Active
|
||||
</span>
|
||||
@elseif($location->license_status === 'pending')
|
||||
<span class="badge badge-warning badge-sm gap-2">
|
||||
<span class="icon-[lucide--clock] size-3"></span>
|
||||
Pending
|
||||
</span>
|
||||
@elseif($location->license_status === 'expired')
|
||||
<span class="badge badge-error badge-sm gap-2">
|
||||
<span class="icon-[lucide--x-circle] size-3"></span>
|
||||
Expired
|
||||
</span>
|
||||
@elseif($location->license_status === 'suspended')
|
||||
<span class="badge badge-error badge-sm gap-2">
|
||||
<span class="icon-[lucide--pause-circle] size-3"></span>
|
||||
Suspended
|
||||
</span>
|
||||
@else
|
||||
<span class="text-base-content/40">Unknown</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($location->license_expiration)
|
||||
<div class="flex items-center gap-2">
|
||||
@if($isExpired)
|
||||
<span class="icon-[lucide--alert-circle] size-4 text-error"></span>
|
||||
<span class="text-error font-medium">
|
||||
{{ $location->license_expiration->format('M d, Y') }}
|
||||
</span>
|
||||
@elseif($isExpiringSoon)
|
||||
<span class="icon-[lucide--alert-triangle] size-4 text-warning"></span>
|
||||
<span class="text-warning font-medium">
|
||||
{{ $location->license_expiration->format('M d, Y') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="icon-[lucide--calendar] size-4 text-base-content/40"></span>
|
||||
{{ $location->license_expiration->format('M d, Y') }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-xs text-base-content/60 mt-1">
|
||||
@if($isExpired)
|
||||
Expired {{ $location->license_expiration->diffForHumans() }}
|
||||
@elseif($isExpiringSoon)
|
||||
Expires {{ $location->license_expiration->diffForHumans() }}
|
||||
@else
|
||||
{{ $location->license_expiration->diffForHumans() }}
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-base-content/40">Not set</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{{ route('seller.business.locations.edit', [$business->slug, $location->id]) }}"
|
||||
class="btn btn-xs btn-ghost">
|
||||
<span class="icon-[lucide--pencil] size-3"></span>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12">
|
||||
<span class="icon-[lucide--map-pin-off] size-12 text-base-content/20 mx-auto mb-4"></span>
|
||||
<h3 class="text-lg font-medium mb-2">No Location Licenses</h3>
|
||||
<p class="text-base-content/60 mb-6">
|
||||
Add locations to track individual licenses for each facility.
|
||||
</p>
|
||||
<a href="{{ route('seller.business.locations.create', $business->slug) }}"
|
||||
class="btn btn-primary">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Your First Location
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compliance Documents -->
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--file-check] size-5 text-primary"></span>
|
||||
Compliance Documents
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Cannabis License -->
|
||||
<div class="flex items-center gap-3 p-4 bg-base-200 rounded-lg">
|
||||
@if($business->cannabis_license_path)
|
||||
<span class="icon-[lucide--check-circle] size-5 text-success"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Cannabis License</div>
|
||||
<div class="text-sm text-base-content/60">Uploaded</div>
|
||||
</div>
|
||||
<a href="{{ Storage::url($business->cannabis_license_path) }}"
|
||||
target="_blank"
|
||||
class="btn btn-xs btn-ghost">
|
||||
View
|
||||
</a>
|
||||
@else
|
||||
<span class="icon-[lucide--alert-circle] size-5 text-warning"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Cannabis License</div>
|
||||
<div class="text-sm text-base-content/60">Not uploaded</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- W9 Form -->
|
||||
<div class="flex items-center gap-3 p-4 bg-base-200 rounded-lg">
|
||||
@if($business->w9_form_path)
|
||||
<span class="icon-[lucide--check-circle] size-5 text-success"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">W9 Form</div>
|
||||
<div class="text-sm text-base-content/60">Uploaded</div>
|
||||
</div>
|
||||
<a href="{{ Storage::url($business->w9_form_path) }}"
|
||||
target="_blank"
|
||||
class="btn btn-xs btn-ghost">
|
||||
View
|
||||
</a>
|
||||
@else
|
||||
<span class="icon-[lucide--alert-circle] size-5 text-warning"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">W9 Form</div>
|
||||
<div class="text-sm text-base-content/60">Not uploaded</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Insurance Certificate -->
|
||||
<div class="flex items-center gap-3 p-4 bg-base-200 rounded-lg">
|
||||
@if($business->insurance_certificate_path)
|
||||
<span class="icon-[lucide--check-circle] size-5 text-success"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Insurance Certificate</div>
|
||||
<div class="text-sm text-base-content/60">Uploaded</div>
|
||||
</div>
|
||||
<a href="{{ Storage::url($business->insurance_certificate_path) }}"
|
||||
target="_blank"
|
||||
class="btn btn-xs btn-ghost">
|
||||
View
|
||||
</a>
|
||||
@else
|
||||
<span class="icon-[lucide--alert-circle] size-5 text-warning"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Insurance Certificate</div>
|
||||
<div class="text-sm text-base-content/60">Not uploaded</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Business License -->
|
||||
<div class="flex items-center gap-3 p-4 bg-base-200 rounded-lg">
|
||||
@if($business->business_license_path)
|
||||
<span class="icon-[lucide--check-circle] size-5 text-success"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Business License</div>
|
||||
<div class="text-sm text-base-content/60">Uploaded</div>
|
||||
</div>
|
||||
<a href="{{ Storage::url($business->business_license_path) }}"
|
||||
target="_blank"
|
||||
class="btn btn-xs btn-ghost">
|
||||
View
|
||||
</a>
|
||||
@else
|
||||
<span class="icon-[lucide--alert-circle] size-5 text-warning"></span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">Business License</div>
|
||||
<div class="text-sm text-base-content/60">Not uploaded</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-base-content/60">
|
||||
<span class="icon-[lucide--info] size-4 inline-block"></span>
|
||||
Upload compliance documents in <a href="{{ route('seller.business.settings.company-information', $business->slug) }}" class="link">Company Information</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,27 +2,264 @@
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Notifications</p>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Email Settings</h1>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Notifications</li>
|
||||
<li><a href="{{ route('seller.business.settings.notifications', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">Notifications</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Notification Preferences</h2>
|
||||
<p class="text-base-content/60">Configure email and system notification preferences.</p>
|
||||
<p class="text-sm text-base-content/60 mb-6">Customize email notification settings.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
<form action="{{ route('seller.business.settings.notifications.update', $business->slug) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- New Order Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">New Order Email Notifications</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Email List -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Addresses</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Comma-separated email addresses to notify when a new order is placed">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="new_order_email_notifications"
|
||||
value="{{ old('new_order_email_notifications', $business->new_order_email_notifications) }}"
|
||||
class="input input-bordered @error('new_order_email_notifications') input-error @enderror"
|
||||
placeholder="email1@example.com, email2@example.com"
|
||||
/>
|
||||
@error('new_order_email_notifications')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Conditional Options -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="new_order_only_when_no_sales_rep"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('new_order_only_when_no_sales_rep', $business->new_order_only_when_no_sales_rep) ? 'checked' : '' }}
|
||||
/>
|
||||
<span class="label-text">Only send New Order Email notifications when no sales reps are assigned to the buyer's account.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="new_order_do_not_send_to_admins"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('new_order_do_not_send_to_admins', $business->new_order_do_not_send_to_admins) ? 'checked' : '' }}
|
||||
/>
|
||||
<span class="label-text">Do not send notifications to company admins.</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Accepted Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Order Accepted Email Notifications</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Email List -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Addresses</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Notify fulfillment and warehouse teams when an order is accepted">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="order_accepted_email_notifications"
|
||||
value="{{ old('order_accepted_email_notifications', $business->order_accepted_email_notifications) }}"
|
||||
class="input input-bordered @error('order_accepted_email_notifications') input-error @enderror"
|
||||
placeholder="fulfillment@example.com, warehouse@example.com"
|
||||
/>
|
||||
@error('order_accepted_email_notifications')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Enable Shipped Emails For Sales Reps -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="enable_shipped_emails_for_sales_reps"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('enable_shipped_emails_for_sales_reps', $business->enable_shipped_emails_for_sales_reps) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Enable Shipped Emails For Sales Reps</span>
|
||||
<p class="text-xs text-base-content/60 mt-1">When checked, sales reps assigned to a customer will receive an email when an order for one of their customers is marked Shipped</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Inquiry Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Platform Inquiry Email Notifications</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Addresses</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Sales reps always get notified. If blank and no sales reps exist, admins are notified.">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="platform_inquiry_email_notifications"
|
||||
value="{{ old('platform_inquiry_email_notifications', $business->platform_inquiry_email_notifications) }}"
|
||||
class="input input-bordered @error('platform_inquiry_email_notifications') input-error @enderror"
|
||||
placeholder="sales@example.com"
|
||||
/>
|
||||
@error('platform_inquiry_email_notifications')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Order Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Manual Order Email Notifications</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="enable_manual_order_email_notifications"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('enable_manual_order_email_notifications', $business->enable_manual_order_email_notifications) ? 'checked' : '' }}
|
||||
/>
|
||||
<span class="label-text font-medium">Enable Manual Order Email Notifications</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="When enabled, all the same emails sent for buyer-created orders will also be sent for orders you create. When disabled, notifications are only sent for buyer-created orders.">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manual_order_emails_internal_only"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('manual_order_emails_internal_only', $business->manual_order_emails_internal_only) ? 'checked' : '' }}
|
||||
/>
|
||||
<span class="label-text font-medium">Manual Order Emails Internal Only</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Email notifications for manual orders will be sent to internal recipients only and not to buyers">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Low Inventory Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Low Inventory Email Notifications</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Addresses</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Notify these addresses when inventory levels are low">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="low_inventory_email_notifications"
|
||||
value="{{ old('low_inventory_email_notifications', $business->low_inventory_email_notifications) }}"
|
||||
class="input input-bordered @error('low_inventory_email_notifications') input-error @enderror"
|
||||
placeholder="inventory@example.com"
|
||||
/>
|
||||
@error('low_inventory_email_notifications')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Certified Seller Status Email Notifications -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Certified Seller Status Email Notifications</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Addresses</span>
|
||||
<span class="label-text-alt tooltip tooltip-right" data-tip="Notify these addresses when certified seller status changes">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="certified_seller_status_email_notifications"
|
||||
value="{{ old('certified_seller_status_email_notifications', $business->certified_seller_status_email_notifications) }}"
|
||||
class="input input-bordered @error('certified_seller_status_email_notifications') input-error @enderror"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
@error('certified_seller_status_email_notifications')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost gap-2">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@@ -2,27 +2,760 @@
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Plans and Billing</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Plans and Billing</li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-60">Plans and Billing</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Subscription and Billing</h2>
|
||||
<p class="text-base-content/60">Manage your subscription plan and billing information.</p>
|
||||
@php
|
||||
// Mock data - replace with actual data from controller
|
||||
$currentPlan = [
|
||||
'name' => 'Marketplace Business',
|
||||
'price' => 395.00,
|
||||
'interval' => 'month',
|
||||
'features' => [
|
||||
'Unlimited products',
|
||||
'Advanced analytics',
|
||||
'Priority support',
|
||||
'Custom branding',
|
||||
],
|
||||
];
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
// Scheduled downgrade (mock - will come from subscription model)
|
||||
$scheduledDowngrade = [
|
||||
'plan_name' => 'Marketplace Standard',
|
||||
'plan_price' => 99.00,
|
||||
'change_date' => '2025-12-17', // Next billing cycle
|
||||
];
|
||||
// Set to null to hide banner: $scheduledDowngrade = null;
|
||||
|
||||
$paymentMethods = [
|
||||
[
|
||||
'id' => 1,
|
||||
'type' => 'card',
|
||||
'brand' => 'Visa',
|
||||
'last4' => '4242',
|
||||
'exp_month' => 12,
|
||||
'exp_year' => 2025,
|
||||
'is_default' => true,
|
||||
'billing_address' => '123 Main St',
|
||||
'billing_city' => 'Phoenix',
|
||||
'billing_state' => 'AZ',
|
||||
'billing_zip' => '85001',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'type' => 'card',
|
||||
'brand' => 'Mastercard',
|
||||
'last4' => '8888',
|
||||
'exp_month' => 6,
|
||||
'exp_year' => 2026,
|
||||
'is_default' => false,
|
||||
'billing_address' => '456 Oak Ave',
|
||||
'billing_city' => 'Scottsdale',
|
||||
'billing_state' => 'AZ',
|
||||
'billing_zip' => '85251',
|
||||
],
|
||||
];
|
||||
|
||||
$billingContacts = [
|
||||
['email' => 'llaz@cannabrands.biz', 'is_primary' => true],
|
||||
['email' => 'accounting@cannabrands.biz', 'is_primary' => false],
|
||||
];
|
||||
|
||||
$invoices = collect([
|
||||
['id' => 'INV-242798', 'date' => '2025-10-17', 'amount' => 99.00, 'status' => 'paid'],
|
||||
['id' => 'INV-240793', 'date' => '2025-09-17', 'amount' => 99.00, 'status' => 'paid'],
|
||||
['id' => 'INV-238747', 'date' => '2025-08-17', 'amount' => 99.00, 'status' => 'paid'],
|
||||
['id' => 'INV-236879', 'date' => '2025-08-01', 'amount' => 99.00, 'status' => 'pending'],
|
||||
['id' => 'INV-235321', 'date' => '2025-07-17', 'amount' => 99.00, 'status' => 'past_due'],
|
||||
['id' => 'INV-233456', 'date' => '2025-06-17', 'amount' => 99.00, 'status' => 'paid'],
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<!-- Scheduled Downgrade Banner -->
|
||||
@if(isset($scheduledDowngrade) && $scheduledDowngrade)
|
||||
<div role="alert" class="alert alert-warning mb-6">
|
||||
<span class="icon-[lucide--clock] size-6"></span>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold">Plan Change Scheduled</h3>
|
||||
<div class="text-sm opacity-80">
|
||||
Your plan will be downgraded to <strong>{{ $scheduledDowngrade['plan_name'] }}</strong>
|
||||
(${{ number_format($scheduledDowngrade['plan_price'], 2) }}/month) on
|
||||
<strong>{{ \Carbon\Carbon::parse($scheduledDowngrade['change_date'])->format('F j, Y') }}</strong>.
|
||||
You'll continue to have access to your current {{ $currentPlan['name'] }} features until then.
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-ghost" onclick="document.getElementById('cancel_downgrade_modal').showModal()">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel Downgrade
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Current Plan and Billing Info Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Current Plan Card -->
|
||||
<div class="card bg-base-100 border border-base-300 h-full">
|
||||
<div class="card-body flex flex-col">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<span class="icon-[lucide--package] size-5 text-primary"></span>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold">Current Plan</h2>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square">
|
||||
<span class="icon-[lucide--edit] size-4"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-2xl font-bold mb-2">{{ $currentPlan['name'] }}</p>
|
||||
<p class="text-base-content/60">
|
||||
<span class="text-xl font-semibold text-base-content">${{ number_format($currentPlan['price'], 2) }}</span> / {{ $currentPlan['interval'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<div class="flex-1 space-y-3 py-4">
|
||||
@foreach($currentPlan['features'] as $feature)
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--check] size-4 text-success flex-shrink-0"></span>
|
||||
<span class="text-sm">{{ $feature }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-4">
|
||||
<button onclick="changePlanModal.showModal()" class="btn btn-outline btn-block gap-2">
|
||||
<span class="icon-[lucide--refresh-cw] size-4"></span>
|
||||
Change Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Info Card -->
|
||||
<div class="card bg-base-100 border border-base-300 h-full">
|
||||
<div class="card-body flex flex-col">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<span class="icon-[lucide--credit-card] size-5 text-primary"></span>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold">Billing</h2>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-square">
|
||||
<span class="icon-[lucide--edit] size-4"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Payment Methods -->
|
||||
<div class="mb-6">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Payment Methods</span>
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
@foreach($paymentMethods as $method)
|
||||
<div class="flex items-center gap-4 p-4 bg-base-200/50 rounded-lg border {{ $method['is_default'] ? 'border-primary' : 'border-base-300' }} cursor-pointer hover:border-primary/50 transition-colors" onclick="setDefaultPaymentMethod({{ $method['id'] }})">
|
||||
<div class="p-2 bg-base-100 rounded">
|
||||
@if($method['brand'] === 'Visa')
|
||||
<span class="icon-[logos--visa] size-6"></span>
|
||||
@elseif($method['brand'] === 'Mastercard')
|
||||
<span class="icon-[logos--mastercard] size-6"></span>
|
||||
@elseif($method['brand'] === 'Amex')
|
||||
<span class="icon-[logos--amex] size-6"></span>
|
||||
@elseif($method['brand'] === 'Discover')
|
||||
<span class="icon-[logos--discover] size-6"></span>
|
||||
@else
|
||||
<span class="icon-[lucide--credit-card] size-6 text-base-content/70"></span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium">{{ $method['brand'] }} •••• {{ $method['last4'] }}</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Expires {{ str_pad($method['exp_month'], 2, '0', STR_PAD_LEFT) }}/{{ $method['exp_year'] }}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/50 mt-1">
|
||||
{{ $method['billing_address'] }}, {{ $method['billing_city'] }}, {{ $method['billing_state'] }} {{ $method['billing_zip'] }}
|
||||
</p>
|
||||
</div>
|
||||
@if($method['is_default'])
|
||||
<span class="badge badge-success badge-sm flex-shrink-0">Default</span>
|
||||
@else
|
||||
<span class="badge badge-ghost badge-sm flex-shrink-0">Click to set default</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Contacts -->
|
||||
<div class="flex-1">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Billing Contacts</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
@foreach($billingContacts as $contact)
|
||||
<div class="flex items-center gap-3 p-3 bg-base-200/50 rounded-lg border border-base-300">
|
||||
<span class="icon-[lucide--mail] size-4 text-base-content/60 flex-shrink-0"></span>
|
||||
<p class="text-sm flex-1 min-w-0 truncate">{{ $contact['email'] }}</p>
|
||||
@if($contact['is_primary'])
|
||||
<span class="badge badge-primary badge-xs flex-shrink-0">Primary</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($billingContacts) < 3)
|
||||
<button onclick="addBillingContactModal.showModal()" class="flex items-center gap-2 p-3 w-full bg-base-200/30 rounded-lg border border-dashed border-base-300 hover:border-primary hover:bg-base-200/50 transition-colors text-sm text-base-content/60 hover:text-primary">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Contact
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pt-6">
|
||||
<button onclick="addPaymentMethodModal.showModal()" class="btn btn-outline btn-block gap-2">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Payment Method
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing History -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold">Billing History</h2>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div class="form-control flex-1">
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="Search by Invoice Number" class="input input-bordered w-full" />
|
||||
<button class="btn btn-primary btn-square">
|
||||
<span class="icon-[lucide--search] size-5"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-outline gap-2">
|
||||
<span class="icon-[lucide--filter] size-4"></span>
|
||||
Filters
|
||||
</button>
|
||||
<select class="select select-bordered">
|
||||
<option>Due Date</option>
|
||||
<option>Amount</option>
|
||||
<option>Status</option>
|
||||
</select>
|
||||
<select class="select select-bordered">
|
||||
<option>Status</option>
|
||||
<option>Paid</option>
|
||||
<option>Pending</option>
|
||||
<option>Past Due</option>
|
||||
<option>Overdue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoices Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
Invoice Number
|
||||
<span class="icon-[lucide--chevrons-up-down] size-4 text-base-content/40"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
Due Date
|
||||
<span class="icon-[lucide--chevrons-up-down] size-4 text-base-content/40"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
Amount Due
|
||||
<span class="icon-[lucide--chevrons-up-down] size-4 text-base-content/40"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($invoices as $invoice)
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<span class="font-medium">{{ $invoice['id'] }}</span>
|
||||
</td>
|
||||
<td>{{ \Carbon\Carbon::parse($invoice['date'])->format('m/d/Y') }}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($invoice['status'] === 'paid')
|
||||
<span class="line-through text-base-content/60 font-medium">${{ number_format($invoice['amount'], 2) }}</span>
|
||||
<div class="tooltip tooltip-success" data-tip="You're awesome! Thanks for helping us keep the lights on!">
|
||||
<span class="badge badge-success badge-sm cursor-help">PAID</span>
|
||||
</div>
|
||||
@elseif($invoice['status'] === 'pending')
|
||||
<span class="font-medium text-warning">${{ number_format($invoice['amount'], 2) }}</span>
|
||||
<div class="tooltip tooltip-warning" data-tip="Please make your payment soon, the natives are getting restless.">
|
||||
<span class="badge badge-warning badge-sm cursor-help">PENDING</span>
|
||||
</div>
|
||||
@elseif($invoice['status'] === 'past_due')
|
||||
<span class="font-medium text-error">${{ number_format($invoice['amount'], 2) }}</span>
|
||||
<div class="tooltip tooltip-error" data-tip="We seriously can't keep going like this.. Please pay your invoice.">
|
||||
<span class="badge badge-error badge-sm cursor-help">PAST DUE</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="font-medium text-error">${{ number_format($invoice['amount'], 2) }}</span>
|
||||
<span class="badge badge-error badge-sm">OVERDUE</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('seller.business.settings.invoice.view', [$business->slug, $invoice['id']]) }}" class="btn btn-ghost btn-sm btn-square" title="View Invoice">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
</a>
|
||||
<a href="{{ route('seller.business.settings.invoice.download', [$business->slug, $invoice['id']]) }}" class="btn btn-ghost btn-sm btn-square" title="Download PDF">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8 text-base-content/60">
|
||||
No invoices found
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-between mt-4 pt-4 border-t border-base-300">
|
||||
<p class="text-sm text-base-content/60">1 - 22 of 22</p>
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm" disabled>
|
||||
<span class="icon-[lucide--chevron-left] size-4"></span>
|
||||
</button>
|
||||
<button class="join-item btn btn-sm btn-active">1</button>
|
||||
<button class="join-item btn btn-sm" disabled>
|
||||
<span class="icon-[lucide--chevron-right] size-4"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Plan Modal -->
|
||||
<dialog id="changePlanModal" class="modal">
|
||||
<div class="modal-box w-11/12 max-w-7xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-2xl">Choose Your Plan</h3>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$availablePlans = [
|
||||
[
|
||||
'id' => 'standard',
|
||||
'name' => 'Standard',
|
||||
'price' => 99,
|
||||
'popular' => false,
|
||||
'features' => [
|
||||
'100 Products',
|
||||
'Basic Analytics',
|
||||
'Email Support',
|
||||
'10 Team Members',
|
||||
'5 GB Storage',
|
||||
'Standard Security',
|
||||
'Mobile App Access',
|
||||
'Monthly Reports',
|
||||
'API Access',
|
||||
'Community Support'
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 'business',
|
||||
'name' => 'Business',
|
||||
'price' => 395,
|
||||
'popular' => true,
|
||||
'features' => [
|
||||
'Unlimited Products',
|
||||
'Advanced Analytics',
|
||||
'Priority Support',
|
||||
'50 Team Members',
|
||||
'50 GB Storage',
|
||||
'Enhanced Security',
|
||||
'Mobile App Access',
|
||||
'Weekly Reports',
|
||||
'Full API Access',
|
||||
'Premium Support'
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 'premium',
|
||||
'name' => 'Premium',
|
||||
'price' => 795,
|
||||
'popular' => false,
|
||||
'features' => [
|
||||
'Unlimited Everything',
|
||||
'AI-Powered Analytics',
|
||||
'Dedicated Support',
|
||||
'Unlimited Team Members',
|
||||
'Unlimited Storage',
|
||||
'Enterprise Security',
|
||||
'White-Label Options',
|
||||
'Real-Time Reports',
|
||||
'Custom Integrations',
|
||||
'24/7 Phone Support'
|
||||
]
|
||||
]
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
@foreach($availablePlans as $plan)
|
||||
<div class="card bg-base-100 border-2 {{ $plan['popular'] ? 'border-primary' : 'border-base-300' }} {{ $currentPlan['name'] === 'Marketplace ' . $plan['name'] ? 'ring-2 ring-success ring-offset-2 ring-offset-base-100' : '' }} relative">
|
||||
@if($plan['popular'])
|
||||
<div class="badge badge-primary absolute -top-3 left-1/2 -translate-x-1/2">Most Popular</div>
|
||||
@endif
|
||||
|
||||
@if($currentPlan['name'] === 'Marketplace ' . $plan['name'])
|
||||
<div class="badge badge-success absolute -top-3 right-4">Current Plan</div>
|
||||
@endif
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="text-2xl font-bold text-center">{{ $plan['name'] }}</h3>
|
||||
<div class="text-center my-4">
|
||||
<span class="text-5xl font-bold">${{ number_format($plan['price']) }}</span>
|
||||
<span class="text-base-content/60">/month</span>
|
||||
</div>
|
||||
|
||||
<div class="divider my-2"></div>
|
||||
|
||||
<ul class="space-y-3 mb-6">
|
||||
@foreach($plan['features'] as $feature)
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="icon-[lucide--check] size-5 text-success flex-shrink-0 mt-0.5"></span>
|
||||
<span class="text-sm">{{ $feature }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@if($currentPlan['name'] === 'Marketplace ' . $plan['name'])
|
||||
<button class="btn btn-success btn-block" disabled>
|
||||
<span class="icon-[lucide--check-circle] size-4"></span>
|
||||
Current Plan
|
||||
</button>
|
||||
@else
|
||||
<form method="POST" action="{{ route('seller.business.settings.plans-and-billing.change-plan', $business->slug) }}" onsubmit="event.stopPropagation();">
|
||||
@csrf
|
||||
<input type="hidden" name="plan_id" value="{{ $plan['id'] }}" />
|
||||
<button type="submit" class="btn {{ $plan['popular'] ? 'btn-primary' : 'btn-outline' }} btn-block">
|
||||
<span class="icon-[lucide--arrow-right] size-4"></span>
|
||||
Select {{ $plan['name'] }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-base-content/60">All plans include a 14-day money-back guarantee</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Add Payment Method Modal -->
|
||||
<dialog id="addPaymentMethodModal" class="modal">
|
||||
<div class="modal-box max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl">Add Payment Method</h3>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="#">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Card Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Card Number</span>
|
||||
</label>
|
||||
<input type="text" name="card_number" placeholder="1234 5678 9012 3456" class="input input-bordered" required maxlength="19" />
|
||||
</div>
|
||||
|
||||
<!-- Card Holder Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Cardholder Name</span>
|
||||
</label>
|
||||
<input type="text" name="card_name" placeholder="John Doe" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<!-- Expiration and CVV -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Expiration</span>
|
||||
</label>
|
||||
<input type="text" name="expiry" placeholder="MM/YY" class="input input-bordered" required maxlength="5" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">CVV</span>
|
||||
</label>
|
||||
<input type="text" name="cvv" placeholder="123" class="input input-bordered" required maxlength="4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider text-sm">Billing Address (for AVS verification)</div>
|
||||
|
||||
<!-- Billing Address -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<input type="text" name="billing_address" placeholder="123 Main St" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<!-- City, State, ZIP -->
|
||||
<div class="grid grid-cols-6 gap-4">
|
||||
<div class="form-control col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input type="text" name="billing_city" placeholder="Phoenix" class="input input-bordered" required />
|
||||
</div>
|
||||
<div class="form-control col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">State</span>
|
||||
</label>
|
||||
<input type="text" name="billing_state" placeholder="AZ" class="input input-bordered" required maxlength="2" />
|
||||
</div>
|
||||
<div class="form-control col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ZIP</span>
|
||||
</label>
|
||||
<input type="text" name="billing_zip" placeholder="85001" class="input input-bordered" required maxlength="10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Set as Default -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="set_default" value="1" class="checkbox checkbox-primary" />
|
||||
<span class="label-text">Set as default payment method</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button type="button" class="btn btn-ghost">Cancel</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--credit-card] size-4"></span>
|
||||
Add Card
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Add Billing Contact Modal -->
|
||||
<dialog id="addBillingContactModal" class="modal">
|
||||
<div class="modal-box max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl">Add Billing Contact</h3>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="#">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Email -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Address</span>
|
||||
</label>
|
||||
<input type="email" name="billing_email" placeholder="billing@example.com" class="input input-bordered" required />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">This contact will receive billing notifications and invoices</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Set as Primary -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="is_primary" value="1" class="checkbox checkbox-primary" />
|
||||
<span class="label-text">Set as primary billing contact</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button type="button" class="btn btn-ghost">Cancel</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--user-plus] size-4"></span>
|
||||
Add Contact
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Cancel Downgrade Modal -->
|
||||
<dialog id="cancel_downgrade_modal" class="modal">
|
||||
<div class="modal-box max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl">Cancel Plan Downgrade</h3>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.settings.plans-and-billing.cancel-downgrade', $business->slug) }}">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm opacity-80">
|
||||
Are you sure you want to cancel your scheduled plan downgrade? Your subscription will remain on the
|
||||
<strong>{{ $currentPlan['name'] }}</strong> plan and you will continue to be billed
|
||||
<strong>${{ number_format($currentPlan['price'], 2) }}/month</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button type="button" class="btn btn-ghost">Keep Downgrade</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--check] size-4"></span>
|
||||
Cancel Downgrade
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function setDefaultPaymentMethod(methodId) {
|
||||
// TODO: Make AJAX call to set default payment method
|
||||
console.log('Setting payment method ' + methodId + ' as default');
|
||||
|
||||
// Example implementation (uncomment when backend is ready):
|
||||
/*
|
||||
fetch(`/seller/{{ $business->slug }}/settings/payment-methods/${methodId}/set-default`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Show success notification and reload page
|
||||
showToast('Payment method set as default', 'success');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error setting default payment method:', error);
|
||||
showToast('Failed to update payment method', 'error');
|
||||
});
|
||||
*/
|
||||
|
||||
// Temporary mock notification
|
||||
showToast('Payment method updated successfully', 'success');
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
// Create toast container if it doesn't exist
|
||||
let toastContainer = document.getElementById('toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'toast-container';
|
||||
toastContainer.className = 'toast toast-top toast-end z-50';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Create toast alert
|
||||
const toast = document.createElement('div');
|
||||
const alertClass = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
|
||||
const iconClass = type === 'success' ? 'icon-[lucide--check-circle]' : type === 'error' ? 'icon-[lucide--x-circle]' : 'icon-[lucide--info]';
|
||||
|
||||
toast.className = `alert ${alertClass}`;
|
||||
toast.innerHTML = `
|
||||
<span class="${iconClass} size-5"></span>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Remove toast after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transition = 'opacity 0.3s';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
467
resources/views/seller/settings/profile.blade.php
Normal file
467
resources/views/seller/settings/profile.blade.php
Normal file
@@ -0,0 +1,467 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">My Profile</h1>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.profile', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">My Profile</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Hero Card with Avatar -->
|
||||
<div class="card bg-gradient-to-br from-primary/10 to-primary/5 border border-primary/20 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col md:flex-row items-center md:items-start gap-8">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content w-32 h-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||
@if(auth()->user()->avatar_path && \Storage::disk('public')->exists(auth()->user()->avatar_path))
|
||||
<img src="{{ asset('storage/' . auth()->user()->avatar_path) }}" alt="{{ auth()->user()->name }}" class="rounded-full">
|
||||
@else
|
||||
<span class="text-5xl font-semibold">
|
||||
{{ strtoupper(substr(auth()->user()->first_name ?? 'U', 0, 1)) }}{{ strtoupper(substr(auth()->user()->last_name ?? 'S', 0, 1)) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Info -->
|
||||
<div class="flex-1 text-center md:text-left">
|
||||
<h2 class="text-3xl font-bold mb-2">{{ auth()->user()->name }}</h2>
|
||||
<p class="text-base-content/70 mb-4">{{ auth()->user()->email }}</p>
|
||||
@if(auth()->user()->position)
|
||||
<div class="badge badge-lg badge-primary badge-outline">{{ auth()->user()->position }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex-shrink-0">
|
||||
<label for="profile-photo-upload" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--camera] size-4"></span>
|
||||
<span>Change Photo</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('seller.business.settings.profile.update', $business->slug) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input type="file" id="profile-photo-upload" name="avatar" accept="image/*" class="hidden"
|
||||
onchange="previewAvatar(this)">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Left Column: Personal Information (2/3 width) -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Personal Information Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--user] size-5 text-primary"></span>
|
||||
Personal Information
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- First Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">First Name</span>
|
||||
</label>
|
||||
<input type="text" name="first_name" value="{{ old('first_name', auth()->user()->first_name) }}"
|
||||
class="input input-bordered @error('first_name') input-error @enderror"
|
||||
placeholder="John" required>
|
||||
@error('first_name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Last Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Last Name</span>
|
||||
</label>
|
||||
<input type="text" name="last_name" value="{{ old('last_name', auth()->user()->last_name) }}"
|
||||
class="input input-bordered @error('last_name') input-error @enderror"
|
||||
placeholder="Doe" required>
|
||||
@error('last_name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Email Address</span>
|
||||
</label>
|
||||
<input type="email" name="email" value="{{ old('email', auth()->user()->email) }}"
|
||||
class="input input-bordered @error('email') input-error @enderror"
|
||||
placeholder="john.doe@example.com" required>
|
||||
@error('email')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Profiles Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--share-2] size-5 text-primary"></span>
|
||||
Social Media Profiles
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- LinkedIn -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--linkedin] size-4"></span>
|
||||
<span>LinkedIn</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="linkedin_url" value="{{ old('linkedin_url', auth()->user()->linkedin_url) }}"
|
||||
class="input input-bordered @error('linkedin_url') input-error @enderror"
|
||||
placeholder="https://linkedin.com/in/yourprofile">
|
||||
@error('linkedin_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Twitter/X -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--twitter] size-4"></span>
|
||||
<span>Twitter / X</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="twitter_url" value="{{ old('twitter_url', auth()->user()->twitter_url) }}"
|
||||
class="input input-bordered @error('twitter_url') input-error @enderror"
|
||||
placeholder="https://twitter.com/yourhandle">
|
||||
@error('twitter_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Facebook -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--facebook] size-4"></span>
|
||||
<span>Facebook</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="facebook_url" value="{{ old('facebook_url', auth()->user()->facebook_url) }}"
|
||||
class="input input-bordered @error('facebook_url') input-error @enderror"
|
||||
placeholder="https://facebook.com/yourprofile">
|
||||
@error('facebook_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Instagram -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--instagram] size-4"></span>
|
||||
<span>Instagram</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="instagram_url" value="{{ old('instagram_url', auth()->user()->instagram_url) }}"
|
||||
class="input input-bordered @error('instagram_url') input-error @enderror"
|
||||
placeholder="https://instagram.com/yourhandle">
|
||||
@error('instagram_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- TikTok -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--video] size-4"></span>
|
||||
<span>TikTok</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="tiktok_url" value="{{ old('tiktok_url', auth()->user()->tiktok_url) }}"
|
||||
class="input input-bordered @error('tiktok_url') input-error @enderror"
|
||||
placeholder="https://tiktok.com/@yourhandle">
|
||||
@error('tiktok_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- GitHub -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
<span class="icon-[lucide--github] size-4"></span>
|
||||
<span>GitHub</span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="github_url" value="{{ old('github_url', auth()->user()->github_url) }}"
|
||||
class="input input-bordered @error('github_url') input-error @enderror"
|
||||
placeholder="https://github.com/yourusername">
|
||||
@error('github_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Avatar Options & Actions (1/3 width) -->
|
||||
<div class="space-y-6">
|
||||
<!-- Avatar Options Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--image-plus] size-5 text-primary"></span>
|
||||
Avatar Options
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
@if(auth()->user()->avatar_path && \Storage::disk('public')->exists(auth()->user()->avatar_path))
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<span class="text-sm">You have a custom avatar uploaded</span>
|
||||
</div>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="remove_avatar" value="1" class="checkbox checkbox-error">
|
||||
<span class="label-text">Remove current avatar</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<div class="divider my-2">OR</div>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="use_gravatar" value="1" class="checkbox checkbox-primary"
|
||||
{{ old('use_gravatar', auth()->user()->use_gravatar) ? 'checked' : '' }}>
|
||||
<span class="label-text">Use Gravatar</span>
|
||||
</label>
|
||||
|
||||
<div class="text-xs text-base-content/60">
|
||||
<span class="icon-[lucide--info] size-3 inline"></span>
|
||||
Gravatar uses your email to display a global avatar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-primary btn-block gap-2">
|
||||
<span class="icon-[lucide--save] size-5"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost btn-block gap-2">
|
||||
<span class="icon-[lucide--x] size-5"></span>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Password Change Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--lock] size-5 text-error"></span>
|
||||
Change Password
|
||||
</h3>
|
||||
|
||||
<form action="{{ route('seller.business.settings.password.update', $business->slug) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Current Password -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Current Password</span>
|
||||
</label>
|
||||
<input type="password" name="current_password"
|
||||
class="input input-bordered @error('current_password') input-error @enderror"
|
||||
placeholder="Enter your current password" required>
|
||||
@error('current_password')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- New Password -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">New Password</span>
|
||||
</label>
|
||||
<input type="password" name="password"
|
||||
class="input input-bordered @error('password') input-error @enderror"
|
||||
placeholder="Enter new password" required>
|
||||
@error('password')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Minimum 8 characters</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Confirm New Password</span>
|
||||
</label>
|
||||
<input type="password" name="password_confirmation"
|
||||
class="input input-bordered"
|
||||
placeholder="Confirm new password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Options -->
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="logout_other_sessions" value="1" class="checkbox checkbox-primary">
|
||||
<span class="label-text">Logout from all other devices after password change</span>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-error gap-2">
|
||||
<span class="icon-[lucide--shield-check] size-4"></span>
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login History -->
|
||||
<div class="card bg-base-100 border border-base-300 mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--history] size-5 text-primary"></span>
|
||||
Login History
|
||||
</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
@if(isset($loginHistory) && $loginHistory->isNotEmpty())
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date & Time</th>
|
||||
<th>IP Address</th>
|
||||
<th>Device / Browser</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($loginHistory as $login)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-medium">{{ $login->created_at->format('M d, Y') }}</div>
|
||||
<div class="text-xs text-base-content/60">{{ $login->created_at->format('h:i A') }}</div>
|
||||
</td>
|
||||
<td class="font-mono text-sm">{{ $login->ip_address }}</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $login->user_agent_parsed ?? 'Unknown Device' }}</div>
|
||||
</td>
|
||||
<td>{{ $login->location ?? 'Unknown' }}</td>
|
||||
<td>
|
||||
@if($login->success)
|
||||
<div class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[lucide--check] size-3"></span>
|
||||
Success
|
||||
</div>
|
||||
@else
|
||||
<div class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--x] size-3"></span>
|
||||
Failed
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($loginHistory->hasPages())
|
||||
<div class="mt-4">
|
||||
{{ $loginHistory->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-12 text-base-content/60">
|
||||
<span class="icon-[lucide--history] size-16 mx-auto mb-4 opacity-30"></span>
|
||||
<p class="text-lg">No login history available</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Preview avatar on file select
|
||||
function previewAvatar(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
// Update all avatar displays
|
||||
document.querySelectorAll('.avatar img, .avatar .bg-primary').forEach(el => {
|
||||
if (el.tagName === 'IMG') {
|
||||
el.src = e.target.result;
|
||||
} else {
|
||||
el.innerHTML = `<img src="${e.target.result}" alt="Preview" class="rounded-full">`;
|
||||
}
|
||||
});
|
||||
}
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
446
resources/views/seller/settings/sales-config.blade.php
Normal file
446
resources/views/seller/settings/sales-config.blade.php
Normal file
@@ -0,0 +1,446 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Sales Configuration</h1>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-60">Sales Config</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('seller.business.settings.sales-config.update', $business->slug) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Order Preferences -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Order Preferences</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Separate Orders by Brand -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="separate_orders_by_brand"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('separate_orders_by_brand', $business->separate_orders_by_brand) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Separate Orders by Brand</span>
|
||||
<p class="text-xs text-base-content/60">Create individual orders for each brand in multi-brand purchases</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Auto Increment Order IDs -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auto_increment_order_ids"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('auto_increment_order_ids', $business->auto_increment_order_ids) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Auto Increment Order IDs</span>
|
||||
<p class="text-xs text-base-content/60">Automatically generate sequential order numbers</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Show Mark as Paid -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="show_mark_as_paid"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('show_mark_as_paid', $business->show_mark_as_paid ?? true) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Show Mark as Paid</span>
|
||||
<p class="text-xs text-base-content/60">Display "Mark as Paid" option in order management</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Display CRM License on Orders -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="display_crm_license_on_orders"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('display_crm_license_on_orders', $business->display_crm_license_on_orders) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Display CRM License on Orders</span>
|
||||
<p class="text-xs text-base-content/60">Show business license number on order documents</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financial Settings -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Financial Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Order Minimum -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Order Minimum</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span class="bg-base-200">$</span>
|
||||
<input
|
||||
type="number"
|
||||
name="order_minimum"
|
||||
value="{{ old('order_minimum', $business->order_minimum) }}"
|
||||
class="input input-bordered w-full @error('order_minimum') input-error @enderror"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Minimum order amount required</span>
|
||||
</label>
|
||||
@error('order_minimum')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Default Shipping Charge -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Default Shipping Charge</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span class="bg-base-200">$</span>
|
||||
<input
|
||||
type="number"
|
||||
name="default_shipping_charge"
|
||||
value="{{ old('default_shipping_charge', $business->default_shipping_charge) }}"
|
||||
class="input input-bordered w-full @error('default_shipping_charge') input-error @enderror"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Standard shipping fee per order</span>
|
||||
</label>
|
||||
@error('default_shipping_charge')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Free Shipping Minimum -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Free Shipping Minimum</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span class="bg-base-200">$</span>
|
||||
<input
|
||||
type="number"
|
||||
name="free_shipping_minimum"
|
||||
value="{{ old('free_shipping_minimum', $business->free_shipping_minimum) }}"
|
||||
class="input input-bordered w-full @error('free_shipping_minimum') input-error @enderror"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Order amount for free shipping</span>
|
||||
</label>
|
||||
@error('free_shipping_minimum')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Documents -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Order Documents</h2>
|
||||
|
||||
<!-- Order Disclaimer -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Order Disclaimer</span>
|
||||
<span class="label-text-alt text-base-content/60">Optional</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="order_disclaimer"
|
||||
rows="4"
|
||||
class="textarea textarea-bordered @error('order_disclaimer') textarea-error @enderror"
|
||||
placeholder="Enter any disclaimer text to appear on orders..."
|
||||
>{{ old('order_disclaimer', $business->order_disclaimer) }}</textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Displayed on order confirmations and invoices</span>
|
||||
</label>
|
||||
@error('order_disclaimer')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Order Invoice Footer -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Order Invoice Footer Copy</span>
|
||||
<span class="label-text-alt text-base-content/60">Optional</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="order_invoice_footer"
|
||||
rows="3"
|
||||
class="textarea textarea-bordered @error('order_invoice_footer') textarea-error @enderror"
|
||||
placeholder="Enter footer text for invoices..."
|
||||
>{{ old('order_invoice_footer', $business->order_invoice_footer) }}</textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Appears at the bottom of all invoices</span>
|
||||
</label>
|
||||
@error('order_invoice_footer')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Management -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Order Management</h2>
|
||||
|
||||
<!-- Prevent Order Editing -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Prevent Order Editing</span>
|
||||
</label>
|
||||
<select
|
||||
name="prevent_order_editing"
|
||||
class="select select-bordered @error('prevent_order_editing') select-error @enderror"
|
||||
>
|
||||
<option value="never" {{ old('prevent_order_editing', $business->prevent_order_editing ?? 'never') == 'never' ? 'selected' : '' }}>
|
||||
Never - Always allow editing
|
||||
</option>
|
||||
<option value="after_approval" {{ old('prevent_order_editing', $business->prevent_order_editing) == 'after_approval' ? 'selected' : '' }}>
|
||||
After Approval - Lock once approved
|
||||
</option>
|
||||
<option value="after_fulfillment" {{ old('prevent_order_editing', $business->prevent_order_editing) == 'after_fulfillment' ? 'selected' : '' }}>
|
||||
After Fulfillment - Lock once fulfilled
|
||||
</option>
|
||||
<option value="always" {{ old('prevent_order_editing', $business->prevent_order_editing) == 'always' ? 'selected' : '' }}>
|
||||
Always - Prevent all editing
|
||||
</option>
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Control when orders can no longer be edited</span>
|
||||
</label>
|
||||
@error('prevent_order_editing')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arizona Compliance Features -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Arizona Compliance Features</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Require Patient Count -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="az_require_patient_count"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('az_require_patient_count', $business->az_require_patient_count) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Require Patient Count</span>
|
||||
<p class="text-xs text-base-content/60">Require customer to provide patient count with orders (medical licenses)</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Require Allotment Verification -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="az_require_allotment_verification"
|
||||
value="1"
|
||||
class="checkbox checkbox-primary"
|
||||
{{ old('az_require_allotment_verification', $business->az_require_allotment_verification) ? 'checked' : '' }}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Require Allotment Verification</span>
|
||||
<p class="text-xs text-base-content/60">Verify customer allotment availability before order confirmation</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Combined Use Payable to Info (Invoice Settings) -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-2">Invoice Payable Info</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">Accounts Payable information for your Company's Combined or Cannabis license orders.<br>If not entered, the default Company name and address will be used.</p>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Company Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="invoice_payable_company_name"
|
||||
value="{{ old('invoice_payable_company_name', $business->invoice_payable_company_name) }}"
|
||||
class="input input-bordered @error('invoice_payable_company_name') input-error @enderror"
|
||||
placeholder="Life Changers Investments DBA Leopard AZ"
|
||||
/>
|
||||
@error('invoice_payable_company_name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Address</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="invoice_payable_address"
|
||||
value="{{ old('invoice_payable_address', $business->invoice_payable_address) }}"
|
||||
class="input input-bordered @error('invoice_payable_address') input-error @enderror"
|
||||
placeholder="1225 W Deer Valley"
|
||||
/>
|
||||
@error('invoice_payable_address')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- City -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="invoice_payable_city"
|
||||
value="{{ old('invoice_payable_city', $business->invoice_payable_city) }}"
|
||||
class="input input-bordered @error('invoice_payable_city') input-error @enderror"
|
||||
placeholder="Phoenix"
|
||||
/>
|
||||
@error('invoice_payable_city')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- State -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">State</span>
|
||||
</label>
|
||||
<select
|
||||
name="invoice_payable_state"
|
||||
class="select select-bordered @error('invoice_payable_state') select-error @enderror"
|
||||
>
|
||||
<option value="">Select state</option>
|
||||
<option value="AZ" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'AZ' ? 'selected' : '' }}>Arizona</option>
|
||||
<option value="CA" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'CA' ? 'selected' : '' }}>California</option>
|
||||
<option value="CO" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'CO' ? 'selected' : '' }}>Colorado</option>
|
||||
<option value="NV" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'NV' ? 'selected' : '' }}>Nevada</option>
|
||||
<option value="NM" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'NM' ? 'selected' : '' }}>New Mexico</option>
|
||||
<option value="OR" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'OR' ? 'selected' : '' }}>Oregon</option>
|
||||
<option value="WA" {{ old('invoice_payable_state', $business->invoice_payable_state) == 'WA' ? 'selected' : '' }}>Washington</option>
|
||||
</select>
|
||||
@error('invoice_payable_state')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Zip Code -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Zip Code</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="invoice_payable_zipcode"
|
||||
value="{{ old('invoice_payable_zipcode', $business->invoice_payable_zipcode) }}"
|
||||
class="input input-bordered @error('invoice_payable_zipcode') input-error @enderror"
|
||||
placeholder="85027"
|
||||
maxlength="10"
|
||||
/>
|
||||
@error('invoice_payable_zipcode')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost gap-2">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
267
resources/views/seller/settings/users-edit.blade.php
Normal file
267
resources/views/seller/settings/users-edit.blade.php
Normal file
@@ -0,0 +1,267 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--user-cog] size-6"></span>
|
||||
<p class="text-lg font-medium">Edit User Permissions</p>
|
||||
</div>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.users', $business->slug) }}">Users</a></li>
|
||||
<li class="opacity-60">Edit Permissions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success mb-6">
|
||||
<span class="icon-[lucide--check-circle] size-5"></span>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- User Info Card -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary/10 text-primary rounded-full w-16 h-16">
|
||||
<span class="text-2xl font-bold">
|
||||
{{ strtoupper(substr($user->first_name ?? 'U', 0, 1) . substr($user->last_name ?? '', 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl font-bold">{{ $user->first_name }} {{ $user->last_name }}</h2>
|
||||
<p class="text-base-content/60">{{ $user->email }}</p>
|
||||
<div class="flex gap-2 mt-2">
|
||||
@if($business->owner_user_id === $user->id)
|
||||
<span class="badge badge-primary gap-1">
|
||||
<span class="icon-[lucide--crown] size-3"></span>
|
||||
Business Owner
|
||||
</span>
|
||||
@elseif($user->pivot->is_primary)
|
||||
<span class="badge badge-primary">Primary Contact</span>
|
||||
@endif
|
||||
@if($user->pivot->contact_type)
|
||||
<span class="badge badge-outline">
|
||||
{{ ucwords(str_replace('_', ' ', $user->pivot->contact_type)) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Owner Warning -->
|
||||
@if($isOwner)
|
||||
<div class="alert alert-info mb-6">
|
||||
<span class="icon-[lucide--shield-check] size-5"></span>
|
||||
<span>This user is the business owner and has full access to all features. Permissions cannot be modified.</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Permissions Form -->
|
||||
<form action="{{ route('seller.business.settings.users.update', [$business->slug, $user]) }}" method="POST" id="permissions-form">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title mb-4">
|
||||
<span class="icon-[lucide--shield] size-5"></span>
|
||||
User Information
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Position -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Position</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="position"
|
||||
value="{{ old('position', $user->position) }}"
|
||||
class="input input-bordered"
|
||||
placeholder="e.g. Sales Manager"
|
||||
{{ $isOwner ? 'disabled' : '' }}>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Job title or role in the company</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Company (from users table) -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Company</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="company"
|
||||
value="{{ old('company', $user->company) }}"
|
||||
class="input input-bordered"
|
||||
placeholder="Company name"
|
||||
{{ $isOwner ? 'disabled' : '' }}>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Company affiliation</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Contact Type -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Contact Type</span>
|
||||
</label>
|
||||
<select name="contact_type" class="select select-bordered" {{ $isOwner ? 'disabled' : '' }}>
|
||||
<option value="">None</option>
|
||||
<option value="primary" {{ $user->pivot->contact_type === 'primary' ? 'selected' : '' }}>Primary Contact</option>
|
||||
<option value="billing" {{ $user->pivot->contact_type === 'billing' ? 'selected' : '' }}>Billing Contact</option>
|
||||
<option value="technical" {{ $user->pivot->contact_type === 'technical' ? 'selected' : '' }}>Technical Contact</option>
|
||||
<option value="owner" {{ $user->pivot->contact_type === 'owner' ? 'selected' : '' }}>Owner</option>
|
||||
<option value="manager" {{ $user->pivot->contact_type === 'manager' ? 'selected' : '' }}>Manager</option>
|
||||
<option value="brand_manager" {{ $user->pivot->contact_type === 'brand_manager' ? 'selected' : '' }}>Brand Manager</option>
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Type of contact in relation to the business</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Role (from pivot table) -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Role</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="role"
|
||||
value="{{ old('role', $user->pivot->role) }}"
|
||||
class="input input-bordered"
|
||||
placeholder="e.g. administrator"
|
||||
{{ $isOwner ? 'disabled' : '' }}>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">User role in the system</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title mb-4">
|
||||
<span class="icon-[lucide--key] size-5"></span>
|
||||
Permissions & Access
|
||||
</h3>
|
||||
|
||||
<!-- Hidden role_template field (kept in database but not shown to users) -->
|
||||
<input type="hidden" name="role_template" value="{{ $user->pivot->role_template }}">
|
||||
|
||||
<!-- Permission Categories -->
|
||||
<div class="space-y-4">
|
||||
@foreach($permissionCategories as $categoryKey => $category)
|
||||
<div class="card bg-base-200/50 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<!-- Category Header -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-semibold flex items-center gap-2">
|
||||
<span class="icon-[{{ $category['icon'] }}] size-5"></span>
|
||||
{{ $category['name'] }}
|
||||
</h4>
|
||||
@if(!$isOwner)
|
||||
<button type="button"
|
||||
class="btn btn-xs btn-ghost enable-all-toggle"
|
||||
data-category="{{ $categoryKey }}"
|
||||
onclick="toggleCategoryPermissions('{{ $categoryKey }}')">
|
||||
<span class="icon-[lucide--check-square] size-3"></span>
|
||||
Enable All
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Permissions Grid (2 columns) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
@foreach($category['permissions'] as $permKey => $permData)
|
||||
<label class="flex items-start gap-2 p-2 rounded hover:bg-base-100 cursor-pointer permission-item"
|
||||
data-category="{{ $categoryKey }}">
|
||||
<input type="checkbox"
|
||||
name="permissions[]"
|
||||
value="{{ $permKey }}"
|
||||
class="checkbox checkbox-sm checkbox-primary mt-0.5"
|
||||
{{ is_array($user->pivot->permissions) && in_array($permKey, $user->pivot->permissions) ? 'checked' : '' }}
|
||||
{{ $isOwner ? 'disabled checked' : '' }}>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium truncate">{{ $permData['name'] }}</div>
|
||||
<div class="text-xs text-base-content/60 line-clamp-2">{{ $permData['description'] }}</div>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center mt-6 pt-6 border-t">
|
||||
<a href="{{ route('seller.business.settings.users', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--arrow-left] size-4"></span>
|
||||
Back to Users
|
||||
</a>
|
||||
@if(!$isOwner)
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
/**
|
||||
* Toggle all permissions in a category
|
||||
*/
|
||||
function toggleCategoryPermissions(categoryKey) {
|
||||
const checkboxes = document.querySelectorAll(`.permission-item[data-category="${categoryKey}"] input[type="checkbox"]`);
|
||||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = !allChecked;
|
||||
});
|
||||
|
||||
updateEnableAllButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update "Enable All" button states based on current selections
|
||||
*/
|
||||
function updateEnableAllButtons() {
|
||||
document.querySelectorAll('.enable-all-toggle').forEach(button => {
|
||||
const categoryKey = button.dataset.category;
|
||||
const checkboxes = document.querySelectorAll(`.permission-item[data-category="${categoryKey}"] input[type="checkbox"]`);
|
||||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||||
|
||||
if (allChecked) {
|
||||
button.innerHTML = '<span class="icon-[lucide--x-square] size-3"></span> Disable All';
|
||||
} else {
|
||||
button.innerHTML = '<span class="icon-[lucide--check-square] size-3"></span> Enable All';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update "Enable All" buttons when individual checkboxes change
|
||||
*/
|
||||
document.querySelectorAll('#permissions-form input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateEnableAllButtons);
|
||||
});
|
||||
|
||||
// Initialize button states on page load
|
||||
updateEnableAllButtons();
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -2,27 +2,879 @@
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Users</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Users</li>
|
||||
</ul>
|
||||
<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>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary gap-2" onclick="add_user_modal.showModal()">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add users
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="{{ route('seller.business.settings.users', $business->slug) }}" class="space-y-4">
|
||||
<!-- Main Search Bar -->
|
||||
<div class="form-control">
|
||||
<div class="input-group">
|
||||
<span class="flex items-center px-4 bg-base-200">
|
||||
<span class="icon-[lucide--search] size-4"></span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="Search users by name or email..."
|
||||
class="input input-bordered flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Last Login Date Range -->
|
||||
<div class="form-control flex-1 min-w-[150px]">
|
||||
<input
|
||||
type="date"
|
||||
name="last_login_start"
|
||||
value="{{ request('last_login_start') }}"
|
||||
placeholder="Login from"
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control flex-1 min-w-[150px]">
|
||||
<input
|
||||
type="date"
|
||||
name="last_login_end"
|
||||
value="{{ request('last_login_end') }}"
|
||||
placeholder="Login to"
|
||||
class="input input-bordered input-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--filter] size-4"></span>
|
||||
Apply Filters
|
||||
</button>
|
||||
<a href="{{ route('seller.business.settings.users', $business->slug) }}" class="btn btn-ghost btn-sm">
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">User Management</h2>
|
||||
<p class="text-base-content/60">Manage users and permissions for your business.</p>
|
||||
<!-- Users Table -->
|
||||
@if($users->count() > 0)
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-lg">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--user] size-4"></span>
|
||||
Name
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--mail] size-4"></span>
|
||||
Email
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--shield] size-4"></span>
|
||||
Role
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--clock] size-4"></span>
|
||||
Last Login
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr class="hover:bg-base-200/50 transition-colors">
|
||||
<td>
|
||||
<div class="font-semibold">{{ $user->name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $user->email }}</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($user->roles->isNotEmpty())
|
||||
@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))
|
||||
};
|
||||
@endphp
|
||||
<div class="badge badge-ghost badge-sm">
|
||||
{{ $displayName }}
|
||||
</div>
|
||||
@else
|
||||
<span class="text-base-content/40">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($user->last_login_at)
|
||||
<div class="text-sm">{{ $user->last_login_at->format('M d, Y') }}</div>
|
||||
<div class="text-xs text-base-content/60">{{ $user->last_login_at->format('g:i A') }}</div>
|
||||
@else
|
||||
<span class="text-base-content/40">Never</span>
|
||||
@endif
|
||||
</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-[lucide--pencil] size-4"></span>
|
||||
Edit
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
<!-- Pagination -->
|
||||
@if($users->hasPages())
|
||||
<div class="flex justify-center border-t border-base-300 p-4 bg-base-50">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<!-- Empty State -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="text-center py-8 text-base-content/60">
|
||||
<span class="icon-[lucide--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']))
|
||||
No users match your filters. Try adjusting your search criteria.
|
||||
@else
|
||||
No users found. Add your first user to get started.
|
||||
@endif
|
||||
</p>
|
||||
</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">
|
||||
<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 mb-6">Add New User</h3>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.settings.users.invite', $business->slug) }}">
|
||||
@csrf
|
||||
|
||||
<!-- 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"
|
||||
required
|
||||
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>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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 class="grid 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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</label>
|
||||
</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" />
|
||||
<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.
|
||||
</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-[lucide--info] 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
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<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-[lucide--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-[lucide--package] 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 LeafLink 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-[lucide--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-[lucide--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-[lucide--mail] 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-[lucide--info] 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-[lucide--lock] 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-[lucide--settings] 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-[lucide--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-[lucide--save] 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-[lucide--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-[lucide--check] size-3"></span>
|
||||
Success
|
||||
</div>
|
||||
@else
|
||||
<div class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--x] 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-[lucide--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
|
||||
|
||||
684
resources/views/seller/settings/webhooks.blade.php
Normal file
684
resources/views/seller/settings/webhooks.blade.php
Normal file
@@ -0,0 +1,684 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--webhook] size-6"></span>
|
||||
<p class="text-lg font-medium">Webhooks / API</p>
|
||||
</div>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-60">Webhooks / API</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Mock data - replace with actual data from controller
|
||||
$webhooks = collect([
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => 'Order Notification',
|
||||
'url' => 'https://example.com/webhooks/orders',
|
||||
'method' => 'POST',
|
||||
'types' => ['order.created', 'order.updated'],
|
||||
'enabled' => true,
|
||||
'last_triggered' => '2025-01-08 14:32:00',
|
||||
'status' => 'success'
|
||||
],
|
||||
]);
|
||||
|
||||
$companyWebhookKey = '1d08cfec92b9ae96e338a9b0350bce034fdf5fdd';
|
||||
$apiKey = '6b9e7b79b4c1994f250a9f394ed6a62adb5d93da';
|
||||
$username = 'kelly@cannabrands.biz';
|
||||
$companySlug = $business->slug;
|
||||
$companyId = $business->id;
|
||||
|
||||
// Available webhook event types
|
||||
$eventTypes = [
|
||||
'Orders' => [
|
||||
'order.created' => 'Order Created',
|
||||
'order.updated' => 'Order Updated',
|
||||
'order.fulfilled' => 'Order Fulfilled',
|
||||
'order.cancelled' => 'Order Cancelled',
|
||||
'order.refunded' => 'Order Refunded',
|
||||
],
|
||||
'Products' => [
|
||||
'product.created' => 'Product Created',
|
||||
'product.updated' => 'Product Updated',
|
||||
'product.deleted' => 'Product Deleted',
|
||||
'inventory.low' => 'Low Inventory Alert',
|
||||
'inventory.out' => 'Out of Stock Alert',
|
||||
],
|
||||
'Customers' => [
|
||||
'customer.created' => 'Customer Created',
|
||||
'customer.updated' => 'Customer Updated',
|
||||
],
|
||||
'Forms' => [
|
||||
'form.submitted' => 'Form Submission',
|
||||
'contact.created' => 'Contact Form Submission',
|
||||
'inquiry.created' => 'Product Inquiry',
|
||||
]
|
||||
];
|
||||
|
||||
// Mock activity log data
|
||||
$activityLog = collect([
|
||||
[
|
||||
'event' => 'webhook.triggered',
|
||||
'webhook_name' => 'Order Notification',
|
||||
'event_type' => 'order.created',
|
||||
'status' => 'success',
|
||||
'response_code' => 200,
|
||||
'timestamp' => '2025-01-09 14:32:15',
|
||||
'duration_ms' => 245,
|
||||
],
|
||||
[
|
||||
'event' => 'webhook.triggered',
|
||||
'webhook_name' => 'Order Notification',
|
||||
'event_type' => 'order.updated',
|
||||
'status' => 'success',
|
||||
'response_code' => 200,
|
||||
'timestamp' => '2025-01-09 12:18:42',
|
||||
'duration_ms' => 198,
|
||||
],
|
||||
[
|
||||
'event' => 'api.request',
|
||||
'endpoint' => '/api/products',
|
||||
'method' => 'GET',
|
||||
'status' => 'success',
|
||||
'response_code' => 200,
|
||||
'timestamp' => '2025-01-09 10:05:33',
|
||||
'duration_ms' => 87,
|
||||
],
|
||||
[
|
||||
'event' => 'webhook.triggered',
|
||||
'webhook_name' => 'Order Notification',
|
||||
'event_type' => 'order.created',
|
||||
'status' => 'failed',
|
||||
'response_code' => 500,
|
||||
'timestamp' => '2025-01-08 16:42:10',
|
||||
'duration_ms' => 1023,
|
||||
'error' => 'Connection timeout',
|
||||
],
|
||||
[
|
||||
'event' => 'api.request',
|
||||
'endpoint' => '/api/orders',
|
||||
'method' => 'POST',
|
||||
'status' => 'success',
|
||||
'response_code' => 201,
|
||||
'timestamp' => '2025-01-08 14:22:05',
|
||||
'duration_ms' => 154,
|
||||
],
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<!-- Webhooks Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--webhook] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">Webhooks</h2>
|
||||
</div>
|
||||
<button onclick="addWebhookModal.showModal()" class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Webhook
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Webhooks Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Method</th>
|
||||
<th>Url</th>
|
||||
<th>Types</th>
|
||||
<th>Last Triggered</th>
|
||||
<th>Status</th>
|
||||
<th>Enabled</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($webhooks as $webhook)
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{{ $webhook['name'] }}</td>
|
||||
<td>
|
||||
<span class="badge badge-ghost badge-sm font-mono">{{ strtoupper($webhook['method']) }}</span>
|
||||
</td>
|
||||
<td class="text-sm font-mono max-w-xs truncate" title="{{ $webhook['url'] }}">
|
||||
{{ $webhook['url'] }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-1 flex-wrap max-w-xs">
|
||||
@foreach($webhook['types'] as $type)
|
||||
<span class="badge badge-sm badge-outline">{{ $type }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm text-base-content/60">
|
||||
@if($webhook['last_triggered'])
|
||||
{{ \Carbon\Carbon::parse($webhook['last_triggered'])->diffForHumans() }}
|
||||
@else
|
||||
<span class="text-base-content/40">Never</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($webhook['status'] === 'success')
|
||||
<div class="tooltip" data-tip="Last delivery successful">
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[lucide--check-circle] size-3"></span>
|
||||
Success
|
||||
</span>
|
||||
</div>
|
||||
@elseif($webhook['status'] === 'failed')
|
||||
<div class="tooltip tooltip-error" data-tip="Last delivery failed">
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--x-circle] size-3"></span>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="badge badge-ghost badge-sm">Pending</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-success toggle-sm" {{ $webhook['enabled'] ? 'checked' : '' }} onchange="toggleWebhook({{ $webhook['id'] }})" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm btn-square">
|
||||
<span class="icon-[lucide--more-vertical] size-4"></span>
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu menu-sm p-2 shadow-lg bg-base-100 rounded-box w-48 border border-base-300">
|
||||
<li><a class="gap-2"><span class="icon-[lucide--edit] size-4"></span> Edit</a></li>
|
||||
<li><a class="gap-2"><span class="icon-[lucide--send] size-4"></span> Test Webhook</a></li>
|
||||
<li><a class="gap-2"><span class="icon-[lucide--history] size-4"></span> View History</a></li>
|
||||
<li class="divider my-0"></li>
|
||||
<li><a class="gap-2 text-error"><span class="icon-[lucide--trash-2] size-4"></span> Delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3 text-base-content/60">
|
||||
<span class="icon-[lucide--webhook] size-12 opacity-40"></span>
|
||||
<p class="font-medium">No webhooks configured</p>
|
||||
<p class="text-sm">Add a webhook to receive real-time notifications about events in your account</p>
|
||||
<button onclick="addWebhookModal.showModal()" class="btn btn-primary btn-sm gap-2 mt-2">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Your First Webhook
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company Webhook Key Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--shield-check] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">Company Webhook Key</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
This value should be used to verify incoming webhooks. All webhooks include a hashed signature in the request header that can be verified with this key.
|
||||
</p>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $companyWebhookKey }}"
|
||||
class="input input-bordered w-full font-mono text-sm pr-24"
|
||||
readonly
|
||||
id="webhookKeyInput"
|
||||
/>
|
||||
<button
|
||||
onclick="copyToClipboard('webhookKeyInput')"
|
||||
class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2 gap-2"
|
||||
>
|
||||
<span class="icon-[lucide--copy] size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $username }}"
|
||||
class="input input-bordered w-full max-w-md"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--refresh-cw] size-4"></span>
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- API Information Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--code-2] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">API</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
The following slug & identifier may be useful when using the API.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Slug</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $companySlug }}"
|
||||
class="input input-bordered w-full font-mono text-sm pr-24"
|
||||
readonly
|
||||
id="companySlugInput"
|
||||
/>
|
||||
<button
|
||||
onclick="copyToClipboard('companySlugInput')"
|
||||
class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2 gap-2"
|
||||
>
|
||||
<span class="icon-[lucide--copy] size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company ID</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $companyId }}"
|
||||
class="input input-bordered w-full font-mono text-sm pr-24"
|
||||
readonly
|
||||
id="companyIdInput"
|
||||
/>
|
||||
<button
|
||||
onclick="copyToClipboard('companyIdInput')"
|
||||
class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2 gap-2"
|
||||
>
|
||||
<span class="icon-[lucide--copy] size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="#" class="btn btn-outline btn-sm gap-2">
|
||||
<span class="icon-[lucide--book-open] size-4"></span>
|
||||
API Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your API Key Card -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--key] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">Your API Key</h2>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Key</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $apiKey }}"
|
||||
class="input input-bordered w-full font-mono text-sm pr-24"
|
||||
readonly
|
||||
id="apiKeyInput"
|
||||
/>
|
||||
<button
|
||||
onclick="copyToClipboard('apiKeyInput')"
|
||||
class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2 gap-2"
|
||||
>
|
||||
<span class="icon-[lucide--copy] size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Username</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value="{{ $username }}"
|
||||
class="input input-bordered w-full font-mono text-sm pr-24"
|
||||
readonly
|
||||
id="apiUsernameInput"
|
||||
/>
|
||||
<button
|
||||
onclick="copyToClipboard('apiUsernameInput')"
|
||||
class="btn btn-ghost btn-sm absolute right-1 top-1/2 -translate-y-1/2 gap-2"
|
||||
>
|
||||
<span class="icon-[lucide--copy] size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<span class="icon-[lucide--alert-triangle] size-4"></span>
|
||||
<span class="text-sm">Keep your API key secure. Do not share it publicly or commit it to version control.</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--refresh-cw] size-4"></span>
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Webhook Modal -->
|
||||
<dialog id="addWebhookModal" class="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="font-bold text-xl">Add Webhook</h3>
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="#">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Webhook Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Webhook Name</span>
|
||||
</label>
|
||||
<input type="text" name="name" placeholder="e.g., Order Created Hook" class="input input-bordered" required />
|
||||
</div>
|
||||
|
||||
<!-- Webhook URL -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">URL</span>
|
||||
</label>
|
||||
<input type="url" name="url" placeholder="https://example.com/webhook" class="input input-bordered" required />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">The endpoint that will receive webhook notifications</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- HTTP Method -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Method</span>
|
||||
</label>
|
||||
<select name="method" class="select select-bordered" required>
|
||||
<option value="POST" selected>POST</option>
|
||||
<option value="GET">GET</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Event Types -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Event Types</span>
|
||||
<span class="label-text-alt">Select events that will trigger this webhook</span>
|
||||
</label>
|
||||
<div class="border border-base-300 rounded-lg p-4 space-y-4 max-h-96 overflow-y-auto">
|
||||
@foreach($eventTypes as $category => $events)
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--folder] size-4 text-primary"></span>
|
||||
<h4 class="font-semibold text-sm">{{ $category }}</h4>
|
||||
</div>
|
||||
<div class="space-y-1 ml-6">
|
||||
@foreach($events as $value => $label)
|
||||
<label class="label cursor-pointer justify-start gap-3 py-1">
|
||||
<input type="checkbox" name="types[]" value="{{ $value }}" class="checkbox checkbox-sm checkbox-primary" />
|
||||
<div class="flex flex-col">
|
||||
<span class="label-text">{{ $label }}</span>
|
||||
<span class="label-text-alt text-xs font-mono opacity-60">{{ $value }}</span>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">You can select multiple event types for one webhook</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Enabled Toggle -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="enabled" value="1" class="toggle toggle-primary" checked />
|
||||
<span class="label-text font-medium">Enable webhook</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button type="button" class="btn btn-ghost">Cancel</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Webhook
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- Activity Log Section -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--activity] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">Activity Log</h2>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Activity Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Details</th>
|
||||
<th>Status</th>
|
||||
<th>Response</th>
|
||||
<th>Duration</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($activityLog as $log)
|
||||
<tr class="hover">
|
||||
<!-- Event Type -->
|
||||
<td>
|
||||
@if($log['event'] === 'webhook.triggered')
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--webhook] size-4 text-primary"></span>
|
||||
<span class="font-medium">Webhook</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--globe] size-4 text-info"></span>
|
||||
<span class="font-medium">API Request</span>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Details -->
|
||||
<td>
|
||||
@if($log['event'] === 'webhook.triggered')
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium">{{ $log['webhook_name'] }}</span>
|
||||
<span class="text-xs text-base-content/60">{{ $log['event_type'] }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col">
|
||||
<span class="badge badge-ghost badge-sm font-mono">{{ strtoupper($log['method']) }}</span>
|
||||
<span class="text-xs font-mono mt-1">{{ $log['endpoint'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td>
|
||||
@if($log['status'] === 'success')
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[lucide--check-circle] size-3"></span>
|
||||
Success
|
||||
</span>
|
||||
@else
|
||||
<div class="tooltip" data-tip="{{ $log['error'] ?? 'Request failed' }}">
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--x-circle] size-3"></span>
|
||||
Failed
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<!-- Response Code -->
|
||||
<td>
|
||||
<span class="font-mono text-sm {{ $log['response_code'] >= 200 && $log['response_code'] < 300 ? 'text-success' : ($log['response_code'] >= 400 ? 'text-error' : 'text-warning') }}">
|
||||
{{ $log['response_code'] }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Duration -->
|
||||
<td class="text-sm text-base-content/60">
|
||||
{{ $log['duration_ms'] }}ms
|
||||
</td>
|
||||
|
||||
<!-- Timestamp -->
|
||||
<td class="text-sm text-base-content/60">
|
||||
{{ \Carbon\Carbon::parse($log['timestamp'])->format('M j, Y g:i A') }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-12">
|
||||
<div class="flex flex-col items-center gap-3 text-base-content/60">
|
||||
<span class="icon-[lucide--activity] size-12 opacity-40"></span>
|
||||
<p class="font-medium">No activity yet</p>
|
||||
<p class="text-sm">Webhook and API activity will appear here</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function copyToClipboard(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
input.select();
|
||||
input.setSelectionRange(0, 99999);
|
||||
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const button = event.target.closest('button');
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<span class="icon-[lucide--check] size-4"></span> Copied!';
|
||||
button.classList.add('btn-success');
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.classList.remove('btn-success');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWebhook(webhookId) {
|
||||
// TODO: Make AJAX call to toggle webhook enabled/disabled status
|
||||
console.log('Toggling webhook ' + webhookId);
|
||||
|
||||
// Example implementation (uncomment when backend is ready):
|
||||
/*
|
||||
fetch(`/seller/{{ $business->slug }}/settings/webhooks/${webhookId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Show success notification
|
||||
console.log('Webhook toggled successfully');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling webhook:', error);
|
||||
// Revert toggle on error
|
||||
event.target.checked = !event.target.checked;
|
||||
});
|
||||
*/
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Storage Test</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="p-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">File Storage Test</h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<span class="text-sm">Current Disk: <strong>{{ config('filesystems.default') }}</strong></span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('storage.test') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Upload Test File</span>
|
||||
</label>
|
||||
<input type="file" name="test_file" class="file-input file-input-bordered" required>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button type="submit" class="btn btn-primary">Test Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divider">Storage Info</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-semibold">Disk</td>
|
||||
<td>{{ config('filesystems.default') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Driver</td>
|
||||
<td>{{ config('filesystems.disks.'.config('filesystems.default').'.driver') }}</td>
|
||||
</tr>
|
||||
@if(config('filesystems.default') === 's3')
|
||||
<tr>
|
||||
<td class="font-semibold">Endpoint</td>
|
||||
<td>{{ config('filesystems.disks.s3.endpoint') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Bucket</td>
|
||||
<td>{{ config('filesystems.disks.s3.bucket') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">URL</td>
|
||||
<td>{{ config('filesystems.disks.s3.url') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -176,7 +176,22 @@ Route::prefix('b')->name('buyer.')->middleware('buyer')->group(function () {
|
||||
Route::post('/invoices/{invoice}/reject', [\App\Http\Controllers\Buyer\InvoiceController::class, 'reject'])->name('business.invoices.reject');
|
||||
Route::post('/invoices/{invoice}/modify', [\App\Http\Controllers\Buyer\InvoiceController::class, 'modify'])->name('business.invoices.modify');
|
||||
|
||||
// Business management (locations, contacts, users) (business-scoped)
|
||||
// ========================================
|
||||
// MODULE ROUTES (Isolated Features)
|
||||
// ========================================
|
||||
// Application has 3 isolated route areas:
|
||||
// - /admin (Superadmin platform management - Filament)
|
||||
// - /b/{business}/* (Buyer/Dispensary modules - DaisyUI, defined below)
|
||||
// - /s/{business}/* (Seller/Brand modules - DaisyUI)
|
||||
//
|
||||
// Buyer modules are isolated for:
|
||||
// - Parallel development without route collisions
|
||||
// - Permission-based access control
|
||||
// - Consistent architecture with seller modules
|
||||
|
||||
// Settings Module (Required)
|
||||
// Always enabled, controlled by role-based permissions
|
||||
// Features: Business profile, locations, contacts, team management
|
||||
Route::prefix('settings')->name('business.')->group(function () {
|
||||
Route::get('/profile', [\App\Http\Controllers\Business\ProfileController::class, 'show'])->name('profile');
|
||||
Route::resource('locations', \App\Http\Controllers\Business\LocationController::class);
|
||||
|
||||
@@ -223,19 +223,97 @@ Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
||||
Route::delete('/{component}', [\App\Http\Controllers\Seller\ComponentController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Settings Management (business-scoped)
|
||||
// ========================================
|
||||
// MODULE ROUTES (Isolated Features)
|
||||
// ========================================
|
||||
// Application has 3 isolated route areas:
|
||||
// - /admin (Superadmin platform management - Filament)
|
||||
// - /b/{business}/* (Buyer/Dispensary modules - DaisyUI)
|
||||
// - /s/{business}/* (Seller/Brand modules - DaisyUI, defined below)
|
||||
//
|
||||
// Each seller module below is isolated for:
|
||||
// - Parallel development without route collisions
|
||||
// - Permission-based access control
|
||||
// - Feature enablement (via business flags or roles)
|
||||
|
||||
// Manufacturing Module (Optional)
|
||||
// Flag: has_manufacturing
|
||||
// Features: Production tracking, batch management, conversions
|
||||
Route::prefix('manufacturing')->name('manufacturing.')->group(function () {
|
||||
// Manufacturing routes will be added here
|
||||
// Examples: batches, wash-reports, conversions, work-orders, boms
|
||||
});
|
||||
|
||||
// Compliance Module (Optional)
|
||||
// Flag: has_compliance
|
||||
// Features: Regulatory tracking, METRC integration, lab results
|
||||
Route::prefix('compliance')->name('compliance.')->group(function () {
|
||||
// Compliance routes will be added here
|
||||
// Examples: metrc, incoming-materials, lab-results, quarantine
|
||||
});
|
||||
|
||||
// Marketing Module (Optional)
|
||||
// Flag: has_marketing
|
||||
// Features: Social media management, campaigns, email marketing
|
||||
Route::prefix('marketing')->name('marketing.')->group(function () {
|
||||
// Marketing routes will be added here
|
||||
// Examples: campaigns, email-marketing, social-media, promotions, customer-segments
|
||||
});
|
||||
|
||||
// Analytics Module (Optional)
|
||||
// Flag: has_analytics
|
||||
// Features: Business intelligence, cross-module reporting, executive dashboards
|
||||
Route::prefix('analytics')->name('analytics.')->group(function () {
|
||||
// Analytics routes will be added here
|
||||
// Examples: overview, sales-analytics, product-analytics, customer-analytics, manufacturing-analytics, marketing-analytics
|
||||
});
|
||||
|
||||
// Settings Module
|
||||
// Always enabled, controlled by role-based permissions
|
||||
// Features: Business configuration, user management, billing
|
||||
Route::prefix('settings')->name('settings.')->group(function () {
|
||||
Route::get('/company-information', [\App\Http\Controllers\Seller\SettingsController::class, 'companyInformation'])->name('company-information');
|
||||
Route::put('/company-information', [\App\Http\Controllers\Seller\SettingsController::class, 'updateCompanyInformation'])->name('company-information.update');
|
||||
Route::get('/users', [\App\Http\Controllers\Seller\SettingsController::class, 'users'])->name('users');
|
||||
Route::get('/orders', [\App\Http\Controllers\Seller\SettingsController::class, 'orders'])->name('orders');
|
||||
Route::get('/sales-config', [\App\Http\Controllers\Seller\SettingsController::class, 'salesConfig'])->name('sales-config');
|
||||
Route::put('/sales-config', [\App\Http\Controllers\Seller\SettingsController::class, 'updateSalesConfig'])->name('sales-config.update');
|
||||
// Legacy routes that redirect to sales-config (orders and invoices were consolidated)
|
||||
Route::get('/orders', function (\App\Models\Business $business) {
|
||||
return redirect()->route('seller.business.settings.sales-config', $business->slug);
|
||||
})->name('orders');
|
||||
Route::get('/invoices', [\App\Http\Controllers\Seller\SettingsController::class, 'invoices'])->name('invoices');
|
||||
Route::get('/brand-kit', [\App\Http\Controllers\Seller\SettingsController::class, 'brandKit'])->name('brand-kit');
|
||||
Route::get('/brands', [\App\Http\Controllers\Seller\SettingsController::class, 'brands'])->name('brands');
|
||||
Route::get('/payments', [\App\Http\Controllers\Seller\SettingsController::class, 'payments'])->name('payments');
|
||||
Route::get('/invoices', [\App\Http\Controllers\Seller\SettingsController::class, 'invoices'])->name('invoices');
|
||||
Route::get('/manage-licenses', [\App\Http\Controllers\Seller\SettingsController::class, 'manageLicenses'])->name('manage-licenses');
|
||||
Route::get('/plans-and-billing', [\App\Http\Controllers\Seller\SettingsController::class, 'plansAndBilling'])->name('plans-and-billing');
|
||||
Route::post('/plans-and-billing/change-plan', [\App\Http\Controllers\Seller\SettingsController::class, 'changePlan'])->name('plans-and-billing.change-plan');
|
||||
Route::post('/plans-and-billing/cancel-downgrade', [\App\Http\Controllers\Seller\SettingsController::class, 'cancelDowngrade'])->name('plans-and-billing.cancel-downgrade');
|
||||
Route::get('/invoice/{invoiceId}', [\App\Http\Controllers\Seller\SettingsController::class, 'viewInvoice'])->name('invoice.view');
|
||||
Route::get('/invoice/{invoiceId}/download', [\App\Http\Controllers\Seller\SettingsController::class, 'downloadInvoice'])->name('invoice.download');
|
||||
Route::get('/notifications', [\App\Http\Controllers\Seller\SettingsController::class, 'notifications'])->name('notifications');
|
||||
Route::put('/notifications', [\App\Http\Controllers\Seller\SettingsController::class, 'updateNotifications'])->name('notifications.update');
|
||||
Route::get('/reports', [\App\Http\Controllers\Seller\SettingsController::class, 'reports'])->name('reports');
|
||||
Route::get('/integrations', [\App\Http\Controllers\Seller\SettingsController::class, 'integrations'])->name('integrations');
|
||||
Route::get('/webhooks', [\App\Http\Controllers\Seller\SettingsController::class, 'webhooks'])->name('webhooks');
|
||||
Route::get('/audit-logs', [\App\Http\Controllers\Seller\SettingsController::class, 'auditLogs'])->name('audit-logs');
|
||||
Route::get('/profile', [\App\Http\Controllers\Seller\SettingsController::class, 'profile'])->name('profile');
|
||||
Route::put('/profile', [\App\Http\Controllers\Seller\SettingsController::class, 'updateProfile'])->name('profile.update');
|
||||
Route::put('/profile/password', [\App\Http\Controllers\Seller\SettingsController::class, 'updatePassword'])->name('password.update');
|
||||
Route::get('/users/edit/{user:uuid}', [\App\Http\Controllers\Seller\SettingsController::class, 'editUser'])->name('users.edit');
|
||||
Route::post('/users/invite', [\App\Http\Controllers\Seller\SettingsController::class, 'inviteUser'])->name('users.invite');
|
||||
Route::patch('/users/{user:uuid}', [\App\Http\Controllers\Seller\SettingsController::class, 'updateUser'])->name('users.update');
|
||||
Route::delete('/users/{user:uuid}', [\App\Http\Controllers\Seller\SettingsController::class, 'removeUser'])->name('users.remove');
|
||||
|
||||
// Category Management (under settings)
|
||||
Route::prefix('categories')->name('categories.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Seller\CategoryController::class, 'index'])->name('index');
|
||||
Route::get('/create/{type}', [\App\Http\Controllers\Seller\CategoryController::class, 'create'])->name('create')->where('type', 'product|component');
|
||||
Route::post('/{type}', [\App\Http\Controllers\Seller\CategoryController::class, 'store'])->name('store')->where('type', 'product|component');
|
||||
Route::get('/{category}/edit', [\App\Http\Controllers\Seller\CategoryController::class, 'edit'])->name('edit');
|
||||
Route::put('/{category}', [\App\Http\Controllers\Seller\CategoryController::class, 'update'])->name('update');
|
||||
Route::delete('/{category}', [\App\Http\Controllers\Seller\CategoryController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\StorageTestController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -199,9 +198,3 @@ Route::prefix('api')->group(function () {
|
||||
->name('api.check-email')
|
||||
->middleware('throttle:10,1'); // Rate limit: 10 requests per minute
|
||||
});
|
||||
|
||||
// Storage Test Routes (Development/Testing Only)
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/storage-test', [StorageTestController::class, 'form'])->name('storage.test.form');
|
||||
Route::post('/storage-test', [StorageTestController::class, 'test'])->name('storage.test');
|
||||
});
|
||||
|
||||
537
tests/Feature/ProductImageControllerTest.php
Normal file
537
tests/Feature/ProductImageControllerTest.php
Normal file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductImage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductImageControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
}
|
||||
|
||||
protected function withCsrfToken(): static
|
||||
{
|
||||
return $this->withSession(['_token' => 'test-token'])
|
||||
->withHeader('X-CSRF-TOKEN', 'test-token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can upload valid product image
|
||||
*/
|
||||
public function test_seller_can_upload_valid_product_image(): void
|
||||
{
|
||||
// Create seller business with brand and product
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create valid test image (750x384 minimum)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was created in database
|
||||
$this->assertDatabaseHas('product_images', [
|
||||
'product_id' => $product->id,
|
||||
'is_primary' => true, // First image should be primary
|
||||
]);
|
||||
|
||||
// Verify file was stored
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
Storage::disk('local')->assertExists($productImage->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test first uploaded image becomes primary
|
||||
*/
|
||||
public function test_first_image_becomes_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
$this->assertTrue($productImage->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test image upload validates minimum dimensions
|
||||
*/
|
||||
public function test_upload_validates_minimum_dimensions(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Image too small (below 750x384)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 500, 300);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test upload validates file type
|
||||
*/
|
||||
public function test_upload_validates_file_type(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Invalid file type
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $file]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot upload more than 6 images per product
|
||||
*/
|
||||
public function test_cannot_upload_more_than_six_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create 6 existing images
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => "products/test-{$i}.jpg",
|
||||
'is_primary' => $i === 0,
|
||||
'sort_order' => $i,
|
||||
]);
|
||||
}
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Maximum of 6 images allowed per product',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot upload image to another business's product (business_id isolation)
|
||||
*/
|
||||
public function test_seller_cannot_upload_image_to_other_business_product(): void
|
||||
{
|
||||
// Create two businesses
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to upload to businessB's product using businessA's slug
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$businessA->slug, $productB->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertNotFound(); // Product not found when scoped to businessA
|
||||
|
||||
// Verify no image was created
|
||||
$this->assertDatabaseMissing('product_images', [
|
||||
'product_id' => $productB->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product's image
|
||||
*/
|
||||
public function test_seller_can_delete_product_image(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create test file
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was deleted from database
|
||||
$this->assertDatabaseMissing('product_images', ['id' => $image->id]);
|
||||
|
||||
// Verify file was deleted from storage
|
||||
Storage::disk('local')->assertMissing('products/test.jpg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting primary image sets next image as primary
|
||||
*/
|
||||
public function test_deleting_primary_image_sets_next_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
Storage::disk('local')->put('products/test1.jpg', 'fake content 1');
|
||||
Storage::disk('local')->put('products/test2.jpg', 'fake content 2');
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Delete primary image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image1->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
// Verify image2 is now primary
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
$imageB = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to delete businessB's product image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$businessA->slug, $productB->id, $imageB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify image was NOT deleted
|
||||
$this->assertDatabaseHas('product_images', ['id' => $imageB->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can reorder product images
|
||||
*/
|
||||
public function test_seller_can_reorder_product_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create three images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$image3 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test3.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Reorder: image3, image1, image2
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$business->slug, $product->id]),
|
||||
['order' => [$image3->id, $image1->id, $image2->id]]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify new sort order
|
||||
$this->assertEquals(0, $image3->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(2, $image2->fresh()->sort_order);
|
||||
|
||||
// Verify first image (image3) is now primary
|
||||
$this->assertTrue($image3->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
$this->assertFalse($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot reorder another business's product images
|
||||
*/
|
||||
public function test_seller_cannot_reorder_other_business_product_images(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to reorder businessB's product images
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$businessA->slug, $productB->id]),
|
||||
['order' => [$image2->id, $image1->id]]
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify order was NOT changed
|
||||
$this->assertEquals(0, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image2->fresh()->sort_order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can set image as primary
|
||||
*/
|
||||
public function test_seller_can_set_image_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Set image2 as primary
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product->id, $image2->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image2 is now primary and image1 is not
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot set primary on another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_set_primary_on_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to set primary on businessB's product image
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$businessA->slug, $productB->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify is_primary was NOT changed
|
||||
$this->assertFalse($image->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot set primary on image that doesn't belong to product
|
||||
*/
|
||||
public function test_cannot_set_primary_on_image_from_different_product(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product1 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
$product2 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create image for product2
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product2->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Try to set it as primary for product1 (wrong product)
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product1->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertStatus(404);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Image not found',
|
||||
]);
|
||||
}
|
||||
}
|
||||
324
tests/Feature/ProductLineControllerTest.php
Normal file
324
tests/Feature/ProductLineControllerTest.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\ProductLine;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductLineControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test seller can create product line for their business
|
||||
*/
|
||||
public function test_seller_can_create_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line created successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name is required
|
||||
*/
|
||||
public function test_product_line_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name must be unique per business
|
||||
*/
|
||||
public function test_product_line_name_must_be_unique_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
// Create existing product line
|
||||
ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name can be duplicated across different businesses
|
||||
*/
|
||||
public function test_product_line_name_can_be_duplicated_across_businesses(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$sellerB = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerB->businesses()->attach($businessB->id);
|
||||
|
||||
// Create product line in business A
|
||||
ProductLine::create([
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
// Create product line with same name in business B (should work)
|
||||
$this->actingAs($sellerB);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success');
|
||||
|
||||
// Verify both exist
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can update their product line
|
||||
*/
|
||||
public function test_seller_can_update_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => 'Ultra Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line updated successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Ultra Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates name is required
|
||||
*/
|
||||
public function test_update_validates_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLine->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates uniqueness per business
|
||||
*/
|
||||
public function test_update_validates_uniqueness_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine1 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$productLine2 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Budget Line',
|
||||
]);
|
||||
|
||||
// Try to rename productLine2 to match productLine1
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine2->id]),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Budget Line', $productLine2->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot update another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_update_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$businessA->slug, $productLineB->id]),
|
||||
['name' => 'Hacked Name']
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLineB->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product line
|
||||
*/
|
||||
public function test_seller_can_delete_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$business->slug, $productLine->id])
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line deleted successfully.');
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$businessA->slug, $productLineB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify product line wasn't deleted
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLineB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller from unauthorized business cannot access another business routes
|
||||
*/
|
||||
public function test_seller_cannot_access_unauthorized_business_routes(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to access businessB routes
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Test Line']
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user