Compare commits
2 Commits
develop
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31a50265ac | ||
|
|
ad849f9bcf |
232
SETTINGS-LOCK.md
Normal file
232
SETTINGS-LOCK.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Settings Protection Guide
|
||||
|
||||
**IMPORTANT**: This document describes how to protect the settings system from being overwritten during branch merges.
|
||||
|
||||
## What's Protected
|
||||
|
||||
The settings system consists of:
|
||||
- **Routes**: `routes/seller.php` (settings prefix section)
|
||||
- **Controller**: `app/Http/Controllers/Seller/SettingsController.php`
|
||||
- **Views**: `resources/views/seller/settings/*.blade.php` (14 files)
|
||||
- **Services**: `app/Services/PermissionService.php`
|
||||
- **Models**: `app/Models/AuditLog.php`
|
||||
- **Components**: `resources/views/components/seller-account-dropdown.blade.php`
|
||||
- **Navigation**: `resources/views/components/seller-sidebar.blade.php`
|
||||
|
||||
## Branch Merge Protection Strategy
|
||||
|
||||
### 1. Before Merging ANY Branch
|
||||
|
||||
Run the validation script to establish a baseline:
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
Expected output: "✓ All checks passed! No issues found."
|
||||
|
||||
If the script fails, **DO NOT PROCEED** with the merge until issues are resolved.
|
||||
|
||||
### 2. During Merge Conflicts
|
||||
|
||||
If you encounter merge conflicts in settings files:
|
||||
|
||||
#### For Routes (`routes/seller.php`)
|
||||
- **Keep**: All routes under the `Route::prefix('settings')` block
|
||||
- **Preserve**: User UUID route binding (lines 39-42)
|
||||
- **Verify**: Settings routes match the list in SETTINGS-VALIDATION.md
|
||||
|
||||
#### For SettingsController
|
||||
- **Keep**: All 25 methods listed in check-settings.sh
|
||||
- **Do NOT remove**: Any method that exists in the current version
|
||||
- **Add**: New methods from incoming branch (if any)
|
||||
|
||||
#### For Views (`resources/views/seller/settings/`)
|
||||
- **Keep**: All 15 blade files (14 settings pages + users-edit)
|
||||
- **Verify**: File sizes match expectations (see validation output)
|
||||
- **Compare**: Use `git diff` to see what's being overwritten
|
||||
|
||||
#### For PermissionService
|
||||
- **Keep**: All 5 categories (products, orders, inventory, customers, settings)
|
||||
- **Keep**: The `audit-logs.view` permission under settings
|
||||
- **Add**: New categories from incoming branch (merge, don't replace)
|
||||
|
||||
### 3. After Merging a Branch
|
||||
|
||||
**IMMEDIATELY** run the validation:
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
If any checks fail:
|
||||
1. Identify what was overwritten using git diff
|
||||
2. Restore from this commit using the recovery commands in SETTINGS-VALIDATION.md
|
||||
3. Re-run validation until all checks pass
|
||||
|
||||
### 4. Git Strategy for Protection
|
||||
|
||||
#### Option A: Create a Protected Settings Branch
|
||||
```bash
|
||||
# Create a protected branch with final settings
|
||||
git checkout -b settings-final
|
||||
git add routes/seller.php app/Http/Controllers/Seller/SettingsController.php
|
||||
git add resources/views/seller/settings/
|
||||
git add app/Services/PermissionService.php app/Models/AuditLog.php
|
||||
git add resources/views/components/seller-account-dropdown.blade.php
|
||||
git add resources/views/components/seller-sidebar.blade.php
|
||||
git commit -m "PROTECTED: Final settings implementation - DO NOT OVERWRITE"
|
||||
|
||||
# Tag it for easy reference
|
||||
git tag -a settings-v1.0 -m "Locked settings implementation"
|
||||
```
|
||||
|
||||
#### Option B: Use Git Attributes (Merge Driver)
|
||||
Create `.gitattributes` in the root:
|
||||
```
|
||||
# Settings files - use "ours" strategy during merge
|
||||
routes/seller.php merge=ours
|
||||
app/Http/Controllers/Seller/SettingsController.php merge=ours
|
||||
resources/views/seller/settings/*.blade.php merge=ours
|
||||
app/Services/PermissionService.php merge=ours
|
||||
```
|
||||
|
||||
Configure the merge driver:
|
||||
```bash
|
||||
git config merge.ours.driver "echo 'Keeping current version'; exit 0"
|
||||
```
|
||||
|
||||
**WARNING**: This will ALWAYS keep your current version, so use carefully!
|
||||
|
||||
#### Option C: Cherry-Pick Strategy (Recommended)
|
||||
Instead of merging entire branches, cherry-pick specific commits:
|
||||
|
||||
```bash
|
||||
# List commits from a branch
|
||||
git log feature-branch --oneline
|
||||
|
||||
# Cherry-pick specific commits that don't touch settings
|
||||
git cherry-pick <commit-hash>
|
||||
|
||||
# If conflicts occur in settings files, abort and skip
|
||||
git cherry-pick --abort
|
||||
```
|
||||
|
||||
### 5. Recovery Protocol
|
||||
|
||||
If settings get overwritten during a merge:
|
||||
|
||||
```bash
|
||||
# 1. Identify the last good commit (before merge)
|
||||
git reflog
|
||||
|
||||
# 2. Restore settings files from the good commit
|
||||
git show <commit-hash>:routes/seller.php > routes/seller.php
|
||||
git show <commit-hash>:app/Http/Controllers/Seller/SettingsController.php > app/Http/Controllers/Seller/SettingsController.php
|
||||
|
||||
# For all views
|
||||
for file in resources/views/seller/settings/*.blade.php; do
|
||||
git show <commit-hash>:$file > $file
|
||||
done
|
||||
|
||||
# 3. Restore services and models
|
||||
git show <commit-hash>:app/Services/PermissionService.php > app/Services/PermissionService.php
|
||||
git show <commit-hash>:app/Models/AuditLog.php > app/Models/AuditLog.php
|
||||
|
||||
# 4. Verify everything is restored
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
## Automated Protection (Pre-Merge Hook)
|
||||
|
||||
Create `.git/hooks/pre-merge-commit`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔒 Running settings validation before merge commit..."
|
||||
|
||||
if bash check-settings.sh; then
|
||||
echo "✅ Settings validation passed"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Settings validation FAILED!"
|
||||
echo "The merge has broken the settings system."
|
||||
echo "Please fix conflicts and run: bash check-settings.sh"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
```bash
|
||||
chmod +x .git/hooks/pre-merge-commit
|
||||
```
|
||||
|
||||
## When Merging PR Branches
|
||||
|
||||
For the 12 PR branches (pr01-pr12):
|
||||
|
||||
1. **Review First**: Check what each PR changes
|
||||
```bash
|
||||
git diff develop...pr01 -- routes/seller.php
|
||||
git diff develop...pr01 -- app/Http/Controllers/Seller/SettingsController.php
|
||||
```
|
||||
|
||||
2. **Selective Merge**: Only merge PRs that don't touch settings
|
||||
```bash
|
||||
# Safe to merge if no output
|
||||
git diff develop...pr01 -- routes/seller.php app/Http/Controllers/Seller/SettingsController.php resources/views/seller/settings/
|
||||
```
|
||||
|
||||
3. **If PR touches settings**: Manual review required
|
||||
- Extract non-settings changes
|
||||
- Apply them separately
|
||||
- Skip settings changes OR carefully merge with validation
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
When making changes to settings files, use this prefix:
|
||||
```
|
||||
SETTINGS: <description>
|
||||
|
||||
Example:
|
||||
SETTINGS: Add analytics permission to PermissionService
|
||||
SETTINGS: Fix brand-kit route references
|
||||
```
|
||||
|
||||
This makes it easy to find settings-related commits:
|
||||
```bash
|
||||
git log --grep="SETTINGS:"
|
||||
```
|
||||
|
||||
## Final Checklist
|
||||
|
||||
Before considering settings "locked":
|
||||
- [ ] Run `bash check-settings.sh` - all checks pass
|
||||
- [ ] All 14 settings pages load without errors
|
||||
- [ ] User permissions system works (can edit user permissions)
|
||||
- [ ] Navigation (sidebar + dropdown) has correct links
|
||||
- [ ] Create git tag: `git tag settings-v1.0`
|
||||
- [ ] Document this commit hash: `git rev-parse HEAD`
|
||||
- [ ] Create backup branch: `git branch settings-backup`
|
||||
|
||||
## Current Settings Commit Hash
|
||||
|
||||
Document the "golden" commit here:
|
||||
```
|
||||
Commit: 84f364de748651950be2404395366f253aef43bb
|
||||
Tag: settings-v1.0
|
||||
Backup Branch: settings-backup
|
||||
Date: Mon, Nov 10, 2025 6:40:53 PM
|
||||
Branch: develop
|
||||
Status: ✅ All 14 settings pages validated and working
|
||||
```
|
||||
|
||||
## Emergency Contacts
|
||||
|
||||
If settings get completely broken:
|
||||
1. Run: `git checkout settings-backup`
|
||||
2. Run: `bash check-settings.sh` to verify
|
||||
3. Cherry-pick needed changes from other branches
|
||||
4. DO NOT merge entire branches
|
||||
|
||||
---
|
||||
|
||||
**Remember**: The settings system is the foundation. Protect it at all costs!
|
||||
110
SETTINGS-PROTECTION-QUICK-REFERENCE.md
Normal file
110
SETTINGS-PROTECTION-QUICK-REFERENCE.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Settings Protection - Quick Reference Card
|
||||
|
||||
## 🔒 Golden Commit
|
||||
```
|
||||
Commit: 84f364de748651950be2404395366f253aef43bb
|
||||
Tag: settings-v1.0
|
||||
Backup: settings-backup branch
|
||||
```
|
||||
|
||||
## ✅ Before ANY Merge
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
```
|
||||
Expected: "✓ All checks passed! No issues found."
|
||||
|
||||
## ⚠️ After EVERY Merge
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
```
|
||||
If it fails, restore immediately!
|
||||
|
||||
## 🚨 Emergency Restore
|
||||
```bash
|
||||
# If settings get broken, restore from backup:
|
||||
git checkout settings-backup -- routes/seller.php
|
||||
git checkout settings-backup -- app/Http/Controllers/Seller/SettingsController.php
|
||||
git checkout settings-backup -- resources/views/seller/settings/
|
||||
git checkout settings-backup -- app/Services/PermissionService.php
|
||||
git checkout settings-backup -- app/Models/AuditLog.php
|
||||
|
||||
# Verify restoration
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
## 📋 Protected Files
|
||||
- `routes/seller.php` - Settings routes
|
||||
- `app/Http/Controllers/Seller/SettingsController.php` - 25 methods
|
||||
- `resources/views/seller/settings/*.blade.php` - 15 views
|
||||
- `app/Services/PermissionService.php` - Permission system
|
||||
- `app/Models/AuditLog.php` - Audit logging
|
||||
- `resources/views/components/seller-account-dropdown.blade.php` - Dropdown menu
|
||||
- `resources/views/components/seller-sidebar.blade.php` - Main navigation
|
||||
|
||||
## 🎯 Safe Merge Strategy
|
||||
```bash
|
||||
# Check what will change BEFORE merging
|
||||
git diff develop...branch-name -- routes/seller.php
|
||||
git diff develop...branch-name -- app/Http/Controllers/Seller/SettingsController.php
|
||||
git diff develop...branch-name -- resources/views/seller/settings/
|
||||
|
||||
# If NO OUTPUT = safe to merge
|
||||
# If OUTPUT = manual review required
|
||||
```
|
||||
|
||||
## 📊 Validation Checklist
|
||||
- [ ] 15 routes (including orders redirect)
|
||||
- [ ] 25 controller methods
|
||||
- [ ] 15 view files
|
||||
- [ ] All views have content (>1000 bytes)
|
||||
- [ ] PermissionService has 5 categories + audit-logs.view
|
||||
- [ ] Navigation links point to correct routes
|
||||
|
||||
## 🔄 Recovery from Tag
|
||||
```bash
|
||||
# Restore all settings from the locked tag
|
||||
git checkout settings-v1.0 -- routes/seller.php \
|
||||
app/Http/Controllers/Seller/SettingsController.php \
|
||||
resources/views/seller/settings/ \
|
||||
app/Services/PermissionService.php \
|
||||
app/Models/AuditLog.php
|
||||
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
## ➕ Adding New Settings (ALLOWED)
|
||||
You CAN add new settings pages/routes/methods. Just ensure:
|
||||
```bash
|
||||
# After adding new settings, update the validation script:
|
||||
# 1. Add new route to EXPECTED_ROUTES array in check-settings.sh
|
||||
# 2. Add new view to EXPECTED_VIEWS array
|
||||
# 3. Add new method to EXPECTED_METHODS array
|
||||
# 4. Run validation to confirm
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
**Example: Adding a new "Security" settings page**
|
||||
1. Add route: `Route::get('/security', ...)->name('security');`
|
||||
2. Add controller method: `public function security(Business $business) { ... }`
|
||||
3. Add view: `resources/views/seller/settings/security.blade.php`
|
||||
4. Update check-settings.sh arrays
|
||||
5. Run validation
|
||||
|
||||
## 💡 Remember
|
||||
**Additions are OK. Overwrites are NOT OK.**
|
||||
|
||||
Rules:
|
||||
- ✅ ADD new routes, methods, views
|
||||
- ✅ ADD new permissions to PermissionService
|
||||
- ❌ REMOVE existing routes, methods, views
|
||||
- ❌ MODIFY existing controller methods without validation
|
||||
- ❌ CHANGE existing view file content without review
|
||||
|
||||
Before ANY merge:
|
||||
1. Running validation BEFORE merge
|
||||
2. Reviewing the changes manually
|
||||
3. Running validation AFTER merge
|
||||
4. Having the backup ready to restore
|
||||
|
||||
---
|
||||
**If in doubt, ask first. It's easier to prevent than to fix!**
|
||||
156
SETTINGS-VALIDATION.md
Normal file
156
SETTINGS-VALIDATION.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Settings Pages Validation
|
||||
|
||||
## Purpose
|
||||
This document describes how to prevent regressions when working on the settings system.
|
||||
|
||||
## Problem
|
||||
When recovering work from git history or making changes to routes/controllers, it's easy to accidentally break existing functionality by:
|
||||
- Missing view files
|
||||
- Missing routes
|
||||
- Missing controller methods
|
||||
- Breaking existing pages while fixing others
|
||||
|
||||
## Solution
|
||||
Use the validation script before committing changes to settings-related code.
|
||||
|
||||
## Usage
|
||||
|
||||
### Run Validation
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
### Expected Output
|
||||
All checks should pass:
|
||||
```
|
||||
======================================
|
||||
Settings Routes & Views Validation
|
||||
======================================
|
||||
...
|
||||
✓ All checks passed! No issues found.
|
||||
```
|
||||
|
||||
### When to Run
|
||||
1. **Before committing** changes to settings-related code
|
||||
2. **After recovering** files from git history
|
||||
3. **After adding** new settings pages
|
||||
4. **When debugging** missing route/view errors
|
||||
|
||||
## What It Checks
|
||||
|
||||
### Routes (in routes/seller.php)
|
||||
All required route names under the `seller.business.settings.*` prefix:
|
||||
- company-information
|
||||
- users
|
||||
- sales-config
|
||||
- orders (legacy redirect)
|
||||
- invoices
|
||||
- brand-kit (branding assets/settings)
|
||||
- payments
|
||||
- manage-licenses
|
||||
- plans-and-billing
|
||||
- notifications
|
||||
- reports
|
||||
- integrations
|
||||
- webhooks
|
||||
- audit-logs
|
||||
- profile
|
||||
|
||||
### Views (in resources/views/seller/settings/)
|
||||
All required Blade templates:
|
||||
- company-information.blade.php
|
||||
- users.blade.php
|
||||
- users-edit.blade.php
|
||||
- sales-config.blade.php
|
||||
- invoices.blade.php
|
||||
- brand-kit.blade.php
|
||||
- payments.blade.php
|
||||
- manage-licenses.blade.php
|
||||
- plans-and-billing.blade.php
|
||||
- notifications.blade.php
|
||||
- reports.blade.php
|
||||
- integrations.blade.php
|
||||
- webhooks.blade.php
|
||||
- audit-logs.blade.php
|
||||
- profile.blade.php
|
||||
|
||||
### Controller Methods (in app/Http/Controllers/Seller/SettingsController.php)
|
||||
All required public methods:
|
||||
- companyInformation()
|
||||
- updateCompanyInformation()
|
||||
- users()
|
||||
- editUser()
|
||||
- inviteUser()
|
||||
- updateUser()
|
||||
- removeUser()
|
||||
- salesConfig()
|
||||
- updateSalesConfig()
|
||||
- invoices()
|
||||
- brandKit()
|
||||
- payments()
|
||||
- manageLicenses()
|
||||
- plansAndBilling()
|
||||
- changePlan()
|
||||
- notifications()
|
||||
- updateNotifications()
|
||||
- reports()
|
||||
- integrations()
|
||||
- webhooks()
|
||||
- auditLogs()
|
||||
- profile()
|
||||
- updateProfile()
|
||||
- updatePassword()
|
||||
|
||||
## Adding New Settings Pages
|
||||
|
||||
When adding a new settings page:
|
||||
|
||||
1. Add the route to `routes/seller.php` in the settings group
|
||||
2. Create the view file in `resources/views/seller/settings/`
|
||||
3. Add the controller method(s) to `SettingsController.php`
|
||||
4. Update `check-settings.sh` arrays:
|
||||
- Add to `EXPECTED_ROUTES`
|
||||
- Add to `EXPECTED_VIEWS`
|
||||
- Add to `EXPECTED_METHODS`
|
||||
5. Run validation: `bash check-settings.sh`
|
||||
6. Update sidebar navigation in `resources/views/components/seller-sidebar.blade.php`
|
||||
7. Update account dropdown if needed in `resources/views/components/seller-account-dropdown.blade.php`
|
||||
|
||||
## Notes
|
||||
|
||||
### Sales Config vs Orders/Invoices
|
||||
- `/sales-config` is the main consolidated settings page
|
||||
- `/orders` redirects to `/sales-config` (legacy compatibility)
|
||||
- `/invoices` is a separate invoice-specific settings page
|
||||
|
||||
### Brands vs Brand Kit
|
||||
- `/brands` (NOT in settings) - Main brand management for CRUD operations on brand entities
|
||||
- `/settings/brand-kit` - Brand assets and branding settings (colors, logos, etc.)
|
||||
|
||||
### Categories
|
||||
Categories have their own controller (`CategoryController`) and are managed separately from the main settings controller.
|
||||
|
||||
## Recovering Lost Work
|
||||
|
||||
If you need to recover lost settings work from git:
|
||||
|
||||
```bash
|
||||
# Find commits with settings changes
|
||||
git log --all --oneline -- "resources/views/seller/settings/"
|
||||
|
||||
# View specific file from commit
|
||||
git show COMMIT_HASH:path/to/file.blade.php
|
||||
|
||||
# Recover file
|
||||
git show COMMIT_HASH:path/to/file.blade.php > path/to/file.blade.php
|
||||
|
||||
# Validate everything is working
|
||||
bash check-settings.sh
|
||||
```
|
||||
|
||||
## Common Git Commits with Settings Work
|
||||
- `70b3be1` - Add settings pages, analytics fixes, and brand management
|
||||
- `9832d8e` - Restore subscription/billing work and settings improvements
|
||||
- `89fbd33` - WIP: Save current work before applying stash
|
||||
- `52facb7` - Merge latest develop branch with conflict resolution
|
||||
- `66d55c4` - Add category management
|
||||
276
WORK-PROTECTION-GUIDE.md
Normal file
276
WORK-PROTECTION-GUIDE.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Work Protection Guide - November 10, 2025
|
||||
|
||||
## 🔒 Golden State Protected
|
||||
|
||||
**Current Branch**: `develop`
|
||||
**Golden Backup**: `golden-backup-20251110`
|
||||
**Working Backup**: `working-backup-20251110`
|
||||
**Settings Backup**: `settings-backup` (from earlier)
|
||||
|
||||
## What's Protected
|
||||
|
||||
### 1. Settings System (15 Routes)
|
||||
- Company Information
|
||||
- Users Management
|
||||
- Sales Config
|
||||
- Invoice Settings
|
||||
- Brand Kit
|
||||
- Payments
|
||||
- Manage Licenses
|
||||
- Plans and Billing
|
||||
- Notifications
|
||||
- Reports
|
||||
- Integrations
|
||||
- Webhooks
|
||||
- Audit Logs
|
||||
- Categories Management
|
||||
- Profile Settings
|
||||
|
||||
**Validation**: Run `bash check-settings.sh`
|
||||
|
||||
### 2. Manufacturing & Compliance
|
||||
- Batch tracking (CRUD operations)
|
||||
- Wash Reports (Stage 1 & 2)
|
||||
- Conversion model with proper fillable fields
|
||||
|
||||
### 3. Brands Management
|
||||
- Full CRUD operations
|
||||
- Preview functionality
|
||||
- Logo and banner uploads
|
||||
|
||||
### 4. Navigation & UI
|
||||
- View Switcher (Sales/Manufacturing/Compliance)
|
||||
- Manufacturing section in sidebar
|
||||
- Brands section in sidebar
|
||||
- All menu items properly initialized
|
||||
|
||||
## 🚨 Emergency Recovery
|
||||
|
||||
### If Anything Breaks
|
||||
|
||||
**Option 1: Restore Everything**
|
||||
```bash
|
||||
# Go back to golden state
|
||||
git checkout golden-backup-20251110
|
||||
|
||||
# Create new working branch
|
||||
git checkout -b fix/restore-working-state
|
||||
|
||||
# When satisfied, merge back
|
||||
git checkout develop
|
||||
git merge fix/restore-working-state
|
||||
```
|
||||
|
||||
**Option 2: Restore Specific Files**
|
||||
```bash
|
||||
# Restore settings only
|
||||
git checkout golden-backup-20251110 -- app/Http/Controllers/Seller/SettingsController.php
|
||||
git checkout golden-backup-20251110 -- resources/views/seller/settings/
|
||||
git checkout golden-backup-20251110 -- routes/seller.php
|
||||
|
||||
# Restore manufacturing
|
||||
git checkout golden-backup-20251110 -- app/Http/Controllers/Seller/BatchController.php
|
||||
git checkout golden-backup-20251110 -- app/Http/Controllers/Seller/WashReportController.php
|
||||
git checkout golden-backup-20251110 -- app/Models/Conversion.php
|
||||
|
||||
# Restore brands
|
||||
git checkout golden-backup-20251110 -- app/Http/Controllers/Seller/BrandController.php
|
||||
git checkout golden-backup-20251110 -- resources/views/seller/brands/
|
||||
```
|
||||
|
||||
**Option 3: Restore Settings (Quick)**
|
||||
```bash
|
||||
git checkout settings-backup -- routes/seller.php
|
||||
git checkout settings-backup -- app/Http/Controllers/Seller/SettingsController.php
|
||||
git checkout settings-backup -- resources/views/seller/settings/
|
||||
git checkout settings-backup -- app/Services/PermissionService.php
|
||||
git checkout settings-backup -- app/Models/AuditLog.php
|
||||
```
|
||||
|
||||
## 📋 Handling Pull Requests
|
||||
|
||||
### Safe PR Merge Strategy
|
||||
|
||||
**BEFORE Merging Any PR:**
|
||||
|
||||
1. **Create test branch**
|
||||
```bash
|
||||
git checkout develop
|
||||
git checkout -b test/pr-merge
|
||||
```
|
||||
|
||||
2. **Check what will change**
|
||||
```bash
|
||||
# For each protected file
|
||||
git diff develop...pr-branch -- routes/seller.php
|
||||
git diff develop...pr-branch -- app/Http/Controllers/Seller/SettingsController.php
|
||||
git diff develop...pr-branch -- resources/views/seller/settings/
|
||||
git diff develop...pr-branch -- app/Http/Controllers/Seller/BatchController.php
|
||||
git diff develop...pr-branch -- app/Http/Controllers/Seller/BrandController.php
|
||||
git diff develop...pr-branch -- resources/views/components/seller-sidebar.blade.php
|
||||
```
|
||||
|
||||
3. **Merge in test branch first**
|
||||
```bash
|
||||
git merge pr-branch
|
||||
|
||||
# If conflicts, resolve carefully
|
||||
# NEVER blindly accept incoming changes to protected files
|
||||
```
|
||||
|
||||
4. **Validate everything works**
|
||||
```bash
|
||||
bash check-settings.sh
|
||||
# Test all pages manually
|
||||
# Check batches, wash reports, brands
|
||||
```
|
||||
|
||||
5. **If validation passes, merge to develop**
|
||||
```bash
|
||||
git checkout develop
|
||||
git merge test/pr-merge
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
6. **If validation fails, restore and investigate**
|
||||
```bash
|
||||
git checkout develop # Abandon test branch
|
||||
# Figure out why it failed before trying again
|
||||
```
|
||||
|
||||
### Protected Files - NEVER Auto-Accept Changes
|
||||
|
||||
These files should NEVER be overwritten by PR merges:
|
||||
|
||||
**Settings Core:**
|
||||
- `routes/seller.php` (lines with settings routes)
|
||||
- `app/Http/Controllers/Seller/SettingsController.php`
|
||||
- `resources/views/seller/settings/*.blade.php`
|
||||
- `app/Services/PermissionService.php`
|
||||
- `app/Models/AuditLog.php`
|
||||
|
||||
**Manufacturing:**
|
||||
- `app/Http/Controllers/Seller/BatchController.php`
|
||||
- `app/Http/Controllers/Seller/WashReportController.php`
|
||||
- `app/Models/Conversion.php`
|
||||
- `resources/views/seller/batches/*.blade.php`
|
||||
- `resources/views/seller/wash-reports/*.blade.php`
|
||||
|
||||
**Brands:**
|
||||
- `app/Http/Controllers/Seller/BrandController.php`
|
||||
- `resources/views/seller/brands/*.blade.php`
|
||||
|
||||
**Navigation:**
|
||||
- `resources/views/components/seller-sidebar.blade.php`
|
||||
- `resources/views/components/view-switcher.blade.php`
|
||||
|
||||
## 🎯 What to Do With PRs Now
|
||||
|
||||
### Step 1: Review All Open PRs
|
||||
```bash
|
||||
# List all PRs
|
||||
gh pr list
|
||||
|
||||
# For each PR, check what files it changes
|
||||
gh pr view <PR_NUMBER> --json files
|
||||
```
|
||||
|
||||
### Step 2: Categorize PRs
|
||||
|
||||
**Safe to Merge** (no conflicts with protected files):
|
||||
- PRs that don't touch routes, controllers, or views we fixed
|
||||
- Documentation updates
|
||||
- Test additions
|
||||
- New features in separate areas
|
||||
|
||||
**Needs Careful Review** (touches protected files):
|
||||
- Any PR modifying settings
|
||||
- Any PR touching navigation
|
||||
- Any PR changing routes
|
||||
|
||||
**Definitely Wait** (conflicts with our work):
|
||||
- PRs that restructure controllers
|
||||
- PRs that change route structure
|
||||
- PRs that modify sidebar/navigation
|
||||
|
||||
### Step 3: Merge Priority
|
||||
|
||||
1. **First**: Safe PRs (no conflicts)
|
||||
2. **Second**: PRs you NEED that have minor conflicts
|
||||
3. **Last**: Major refactoring PRs
|
||||
|
||||
### Step 4: For Each PR
|
||||
|
||||
```bash
|
||||
# Create isolated test branch
|
||||
git checkout develop
|
||||
git checkout -b test/pr-<number>
|
||||
|
||||
# Fetch PR
|
||||
gh pr checkout <number>
|
||||
|
||||
# Check diff
|
||||
git diff develop
|
||||
|
||||
# If looks safe, try merge
|
||||
git checkout test/pr-<number>
|
||||
git merge develop
|
||||
|
||||
# Test everything
|
||||
bash check-settings.sh
|
||||
|
||||
# If good, merge
|
||||
git checkout develop
|
||||
git merge test/pr-<number>
|
||||
git push
|
||||
```
|
||||
|
||||
## 🛡️ Daily Protection Routine
|
||||
|
||||
**Every day before starting work:**
|
||||
```bash
|
||||
# Verify you're on develop
|
||||
git branch --show-current
|
||||
|
||||
# Check status
|
||||
git status
|
||||
|
||||
# Verify backups exist
|
||||
git branch | grep backup
|
||||
|
||||
# If everything looks good, you're safe to work
|
||||
```
|
||||
|
||||
**After making changes:**
|
||||
```bash
|
||||
# Commit frequently
|
||||
git add -A
|
||||
git commit -m "descriptive message"
|
||||
|
||||
# Push to remote (additional safety)
|
||||
git push origin develop
|
||||
```
|
||||
|
||||
## 📞 Recovery Contacts
|
||||
|
||||
**If you get stuck:**
|
||||
1. DON'T PANIC
|
||||
2. DON'T run `git reset --hard`
|
||||
3. DON'T force push
|
||||
4. Create new branch from current state: `git checkout -b emergency-save`
|
||||
5. Then restore from golden backup
|
||||
|
||||
## 🎖️ Golden Rules
|
||||
|
||||
1. **NEVER** merge PRs directly into develop without testing
|
||||
2. **ALWAYS** test in a separate branch first
|
||||
3. **ALWAYS** run validation after any merge
|
||||
4. **ALWAYS** keep golden-backup branch untouched
|
||||
5. **NEVER** force push to develop
|
||||
6. **COMMIT** frequently (can't lose committed work)
|
||||
|
||||
---
|
||||
|
||||
**Created**: November 10, 2025
|
||||
**Last Updated**: November 10, 2025
|
||||
**Status**: ✅ All systems working and protected
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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,32 @@ 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'));
|
||||
// TODO: License table is currently a placeholder - needs migration update
|
||||
// For now, return empty collection so the UI displays properly
|
||||
$licenses = collect([]);
|
||||
|
||||
return view('seller.settings.manage-licenses', compact('business', 'licenses'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +388,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 +540,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 +606,156 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Http/Controllers/ViewSwitcherController.php
Normal file
56
app/Http/Controllers/ViewSwitcherController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\BusinessHelper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ViewSwitcherController extends Controller
|
||||
{
|
||||
/**
|
||||
* Switch the active view (sales/manufacturing/compliance) for the current session
|
||||
*/
|
||||
public function switch(Request $request)
|
||||
{
|
||||
$view = $request->input('view');
|
||||
|
||||
// Validate view
|
||||
if (! in_array($view, ['sales', 'manufacturing', 'compliance'])) {
|
||||
return back()->with('error', 'Invalid view selected');
|
||||
}
|
||||
|
||||
$business = BusinessHelper::current();
|
||||
|
||||
if (! $business) {
|
||||
return back()->with('error', 'No business context');
|
||||
}
|
||||
|
||||
// Check if business has access to this view
|
||||
if ($view === 'manufacturing' && ! $business->has_manufacturing) {
|
||||
return back()->with('error', 'Manufacturing module not enabled for this business');
|
||||
}
|
||||
|
||||
if ($view === 'compliance' && ! $business->has_compliance) {
|
||||
return back()->with('error', 'Compliance module not enabled for this business');
|
||||
}
|
||||
|
||||
// Store selected view in session
|
||||
session(['current_view' => $view]);
|
||||
|
||||
$viewNames = [
|
||||
'sales' => 'Sales',
|
||||
'manufacturing' => 'Manufacturing',
|
||||
'compliance' => 'Compliance',
|
||||
];
|
||||
|
||||
return back()->with('success', 'Switched to '.$viewNames[$view].' view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected view
|
||||
*/
|
||||
public static function getCurrentView(): string
|
||||
{
|
||||
return session('current_view', 'sales');
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace App\Models;
|
||||
use Database\Factories\UserFactory;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -19,7 +20,26 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, HasRoles, Impersonate, Notifiable;
|
||||
use HasFactory, HasRoles, HasUuids, Impersonate, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the columns that should receive a unique identifier.
|
||||
*/
|
||||
public function uniqueIds(): array
|
||||
{
|
||||
return ['uuid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new UUID for the model (first 16 hex chars = 18 total with hyphens).
|
||||
*/
|
||||
public function newUniqueId(): string
|
||||
{
|
||||
$fullUuid = (string) \Illuminate\Support\Str::uuid();
|
||||
|
||||
// Return first 16 hex characters (8+4+4): 550e8400-e29b-41d4
|
||||
return substr($fullUuid, 0, 18); // 18 chars total (16 hex + 2 hyphens)
|
||||
}
|
||||
|
||||
/**
|
||||
* User type constants
|
||||
|
||||
209
app/Services/PermissionService.php
Normal file
209
app/Services/PermissionService.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/**
|
||||
* Permission Service
|
||||
*
|
||||
* Handles role templates and permission management for businesses.
|
||||
*/
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* Get available role templates
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
3
categories-with-type.json
Normal file
3
categories-with-type.json
Normal file
File diff suppressed because one or more lines are too long
127
check-settings.sh
Normal file
127
check-settings.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# Settings Routes and Views Validation Script
|
||||
# Run this script to verify all settings pages are properly configured
|
||||
|
||||
echo "======================================"
|
||||
echo "Settings Routes & Views Validation"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# Expected routes from routes/seller.php (within settings prefix)
|
||||
declare -a EXPECTED_ROUTES=(
|
||||
"company-information"
|
||||
"users"
|
||||
"sales-config"
|
||||
"orders"
|
||||
"invoices"
|
||||
"brand-kit"
|
||||
"payments"
|
||||
"manage-licenses"
|
||||
"plans-and-billing"
|
||||
"notifications"
|
||||
"reports"
|
||||
"integrations"
|
||||
"webhooks"
|
||||
"audit-logs"
|
||||
"profile"
|
||||
)
|
||||
|
||||
# Expected view files
|
||||
declare -a EXPECTED_VIEWS=(
|
||||
"company-information.blade.php"
|
||||
"users.blade.php"
|
||||
"users-edit.blade.php"
|
||||
"sales-config.blade.php"
|
||||
"invoices.blade.php"
|
||||
"brand-kit.blade.php"
|
||||
"payments.blade.php"
|
||||
"manage-licenses.blade.php"
|
||||
"plans-and-billing.blade.php"
|
||||
"notifications.blade.php"
|
||||
"reports.blade.php"
|
||||
"integrations.blade.php"
|
||||
"webhooks.blade.php"
|
||||
"audit-logs.blade.php"
|
||||
"profile.blade.php"
|
||||
)
|
||||
|
||||
# Expected controller methods
|
||||
declare -a EXPECTED_METHODS=(
|
||||
"companyInformation"
|
||||
"updateCompanyInformation"
|
||||
"users"
|
||||
"editUser"
|
||||
"inviteUser"
|
||||
"updateUser"
|
||||
"removeUser"
|
||||
"salesConfig"
|
||||
"updateSalesConfig"
|
||||
"invoices"
|
||||
"brandKit"
|
||||
"payments"
|
||||
"manageLicenses"
|
||||
"plansAndBilling"
|
||||
"changePlan"
|
||||
"notifications"
|
||||
"updateNotifications"
|
||||
"reports"
|
||||
"integrations"
|
||||
"webhooks"
|
||||
"auditLogs"
|
||||
"profile"
|
||||
"updateProfile"
|
||||
"updatePassword"
|
||||
)
|
||||
|
||||
echo "Checking Routes..."
|
||||
echo "------------------"
|
||||
for route in "${EXPECTED_ROUTES[@]}"; do
|
||||
if grep -q "name('$route')" routes/seller.php; then
|
||||
echo "✓ Route: $route"
|
||||
else
|
||||
echo "✗ MISSING Route: $route"
|
||||
((ERRORS++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Checking Views..."
|
||||
echo "------------------"
|
||||
for view in "${EXPECTED_VIEWS[@]}"; do
|
||||
if [ -f "resources/views/seller/settings/$view" ]; then
|
||||
SIZE=$(stat -c%s "resources/views/seller/settings/$view" 2>/dev/null || stat -f%z "resources/views/seller/settings/$view" 2>/dev/null)
|
||||
if [ "$SIZE" -gt 100 ]; then
|
||||
echo "✓ View: $view ($SIZE bytes)"
|
||||
else
|
||||
echo "⚠ View exists but seems empty: $view ($SIZE bytes)"
|
||||
((ERRORS++))
|
||||
fi
|
||||
else
|
||||
echo "✗ MISSING View: $view"
|
||||
((ERRORS++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Checking Controller Methods..."
|
||||
echo "------------------------------"
|
||||
for method in "${EXPECTED_METHODS[@]}"; do
|
||||
if grep -q "function $method" app/Http/Controllers/Seller/SettingsController.php; then
|
||||
echo "✓ Method: $method"
|
||||
else
|
||||
echo "✗ MISSING Method: $method"
|
||||
((ERRORS++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
if [ $ERRORS -eq 0 ]; then
|
||||
echo "✓ All checks passed! No issues found."
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Found $ERRORS issue(s) that need attention."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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::create('component_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('business_id')->constrained('businesses')->cascadeOnDelete();
|
||||
$table->foreignId('parent_id')->nullable()->constrained('component_categories')->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('public')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('business_id');
|
||||
$table->index('parent_id');
|
||||
$table->index('public');
|
||||
$table->index('sort_order');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('component_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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::create('product_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['business_id', 'is_active']);
|
||||
$table->index('name');
|
||||
|
||||
// Unique constraint: prevent duplicate category names per business
|
||||
$table->unique(['business_id', 'name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
// Add UUID column (16 hex chars = 18 total with hyphens)
|
||||
$table->char('uuid', 18)->nullable()->after('id'); // Format: 550e8400-e29b-41d4
|
||||
$table->unique('uuid');
|
||||
$table->index('uuid');
|
||||
});
|
||||
|
||||
// Generate UUIDs for existing users (first 16 hex chars)
|
||||
$users = DB::table('users')->get();
|
||||
foreach ($users as $user) {
|
||||
$fullUuid = (string) Str::uuid();
|
||||
// Extract first 16 hex characters (8+4+4): 550e8400-e29b-41d4
|
||||
$shortUuid = substr($fullUuid, 0, 18); // 18 chars total (16 hex + 2 hyphens)
|
||||
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->update(['uuid' => $shortUuid]);
|
||||
}
|
||||
|
||||
// Make UUID non-nullable after populating existing records
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->char('uuid', 18)->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropUnique(['uuid']);
|
||||
$table->dropIndex(['uuid']);
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('products', function (Blueprint $table) {
|
||||
$table->foreignId('product_category_id')->nullable()->after('category')->constrained('product_categories')->nullOnDelete();
|
||||
$table->index('product_category_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropForeign(['product_category_id']);
|
||||
$table->dropIndex(['product_category_id']);
|
||||
$table->dropColumn('product_category_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('product_categories', function (Blueprint $table) {
|
||||
$table->string('slug')->nullable()->after('name');
|
||||
$table->integer('sort_order')->nullable()->after('description');
|
||||
$table->foreignId('parent_id')->nullable()->after('sort_order')->constrained('product_categories')->nullOnDelete();
|
||||
|
||||
$table->index('parent_id');
|
||||
$table->index('slug');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('product_categories', function (Blueprint $table) {
|
||||
$table->dropForeign(['parent_id']);
|
||||
$table->dropIndex(['parent_id']);
|
||||
$table->dropIndex(['slug']);
|
||||
$table->dropColumn(['slug', 'sort_order', 'parent_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('components', function (Blueprint $table) {
|
||||
$table->foreignId('component_category_id')->nullable()->after('business_id')->constrained('component_categories')->nullOnDelete();
|
||||
$table->index('component_category_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('components', function (Blueprint $table) {
|
||||
$table->dropForeign(['component_category_id']);
|
||||
$table->dropIndex(['component_category_id']);
|
||||
$table->dropColumn('component_category_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('component_categories', function (Blueprint $table) {
|
||||
$table->boolean('is_active')->default(true)->after('description');
|
||||
$table->string('slug')->nullable()->after('name');
|
||||
$table->dropColumn('public');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('component_categories', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_active', 'slug']);
|
||||
$table->boolean('public')->default(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
246
import-categories.php
Normal file
246
import-categories.php
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Import Product Categories from MySQL JSON export to PostgreSQL
|
||||
*
|
||||
* This script imports categories from categories.json and creates them
|
||||
* in the product_categories table, associating them with the appropriate business.
|
||||
*
|
||||
* Usage:
|
||||
* Run in artisan tinker:
|
||||
* php artisan tinker --execute="require 'import-categories.php';"
|
||||
*/
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\ProductCategory;
|
||||
use App\Models\ComponentCategory;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
echo "=== CATEGORY IMPORT SCRIPT ===\n\n";
|
||||
|
||||
// Load the JSON data
|
||||
$jsonFile = base_path('categories-with-type.json');
|
||||
if (!file_exists($jsonFile)) {
|
||||
die("ERROR: categories-with-type.json not found in project root!\n");
|
||||
}
|
||||
|
||||
$categoriesJson = file_get_contents($jsonFile);
|
||||
$mysqlCategories = json_decode($categoriesJson, true);
|
||||
|
||||
if (!$mysqlCategories) {
|
||||
die("ERROR: Failed to parse categories-with-type.json!\n");
|
||||
}
|
||||
|
||||
echo "Loaded " . count($mysqlCategories) . " categories from MySQL export\n\n";
|
||||
|
||||
// We need to associate categories with a business
|
||||
// For CannaB rands, find by slug
|
||||
echo "Looking for CannaB rands business...\n";
|
||||
|
||||
$business = Business::where('slug', 'cannabrands')->first();
|
||||
if (!$business) {
|
||||
// Fallback: try to find by name
|
||||
$business = Business::where('name', 'LIKE', '%canna%brand%')->first();
|
||||
}
|
||||
|
||||
if (!$business) {
|
||||
die("ERROR: CannaB rands business not found! Please check the business slug or name.\n");
|
||||
}
|
||||
|
||||
echo "✓ Found business: {$business->name} (ID: {$business->id}, Slug: {$business->slug})\n\n";
|
||||
|
||||
// Category type mapping based on category names
|
||||
// We'll categorize them based on their usage in products vs components
|
||||
$categoryTypeMap = [
|
||||
// Product categories (based on Hash Factory categories and common cannabis categories)
|
||||
'Rosin' => 'product',
|
||||
'Live Rosin' => 'product',
|
||||
'Live Hash Rosin' => 'product',
|
||||
'Cured Hash' => 'product',
|
||||
'Live Hash' => 'product',
|
||||
'BHO' => 'product',
|
||||
'Fresh Press' => 'product',
|
||||
'Cold Cure' => 'product',
|
||||
'Green Hash' => 'product',
|
||||
'Blonde Hash' => 'product',
|
||||
'Cured Rosin' => 'product',
|
||||
'Live Resin' => 'product',
|
||||
'Cured Resin' => 'product',
|
||||
'Distillate' => 'product',
|
||||
'Shatter' => 'product',
|
||||
'Wax' => 'product',
|
||||
'Budder' => 'product',
|
||||
'Badder' => 'product',
|
||||
'Crumble' => 'product',
|
||||
'Sugar' => 'product',
|
||||
'Diamonds' => 'product',
|
||||
'Hash' => 'product',
|
||||
'Kief' => 'product',
|
||||
'Oil' => 'product',
|
||||
|
||||
// Flower categories
|
||||
'Flower' => 'product',
|
||||
'Bulk Flower' => 'product',
|
||||
'Packaged Flower' => 'product',
|
||||
'Cured Flower' => 'product',
|
||||
'Fresh Frozen' => 'product',
|
||||
'Shake' => 'product',
|
||||
'Trim' => 'product',
|
||||
'Clones' => 'product',
|
||||
'Seeds' => 'product',
|
||||
|
||||
// Pre-Roll categories
|
||||
'Pre-Rolls' => 'product',
|
||||
'Blunts' => 'product',
|
||||
'Infused' => 'product',
|
||||
'Non-Infused' => 'product',
|
||||
|
||||
// Edibles
|
||||
'Edibles' => 'product',
|
||||
'Edibles & Ingestibles' => 'product',
|
||||
'Gummies' => 'product',
|
||||
'Chocolate' => 'product',
|
||||
'Cookies' => 'product',
|
||||
'Baked Goods (non-cookie)' => 'product',
|
||||
'Beverages' => 'product',
|
||||
'Capsules' => 'product',
|
||||
'Tinctures' => 'product',
|
||||
|
||||
// Topicals
|
||||
'Topicals' => 'product',
|
||||
'Lotions & Creams' => 'product',
|
||||
'Balms/Salves' => 'product',
|
||||
|
||||
// Cartridges
|
||||
'Cartridges' => 'product',
|
||||
'510 Thread' => 'product',
|
||||
'Disposable' => 'product',
|
||||
|
||||
// Component/Packaging categories (anything that's raw materials or packaging)
|
||||
'Packaging' => 'component',
|
||||
'Pop Top Pre Roll Child Resistant Tubes' => 'component',
|
||||
'Concentrate Jar' => 'component',
|
||||
'Push-Up Tube' => 'component',
|
||||
'Mylar Bags' => 'component',
|
||||
'Multipack Pre-Roll Bottle' => 'component',
|
||||
'Multipack Pre-Roll Lid' => 'component',
|
||||
'Shrink Bands' => 'component',
|
||||
'Seals' => 'component',
|
||||
'Bottles' => 'component',
|
||||
'Drams' => 'component',
|
||||
'Boxes' => 'component',
|
||||
'Pre-Roll Cones' => 'component',
|
||||
'Treefer 109/26' => 'component',
|
||||
'Treefer 84/26' => 'component',
|
||||
'Treefer 70/26' => 'component',
|
||||
'Hemp Blunts' => 'component',
|
||||
'Glass Tube - Corks' => 'component',
|
||||
|
||||
// Accessories (typically components/materials)
|
||||
'Accessories' => 'component',
|
||||
'Apperal' => 'component',
|
||||
'Batteries' => 'component',
|
||||
'Bongs' => 'component',
|
||||
'Grinders' => 'component',
|
||||
'Lighters' => 'component',
|
||||
'Pipes' => 'component',
|
||||
'Rigs' => 'component',
|
||||
'Rolling Papers & Trays' => 'component',
|
||||
'Storage' => 'component',
|
||||
'Vaporizers' => 'component',
|
||||
|
||||
// Lab/Testing/Additives
|
||||
'Lab Testing' => 'component',
|
||||
'Additives' => 'component',
|
||||
'Terpenes' => 'component',
|
||||
|
||||
// Food ingredients (components for edibles)
|
||||
'Tea Bags' => 'component',
|
||||
'Juice' => 'component',
|
||||
'Puree Mix' => 'component',
|
||||
'Coffee' => 'component',
|
||||
|
||||
// Bulk/Raw materials
|
||||
'Bulk' => 'component',
|
||||
'Concentrates' => 'product', // This one could be either, defaulting to product
|
||||
];
|
||||
|
||||
// Stats
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
|
||||
echo "Starting import...\n";
|
||||
echo "================\n\n";
|
||||
|
||||
foreach ($mysqlCategories as $mysqlCategory) {
|
||||
// Use type from JSON to determine which model to use
|
||||
$type = $mysqlCategory['type'];
|
||||
|
||||
// Choose the correct model based on type
|
||||
$modelClass = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$typeName = $type === 'product' ? 'Product' : 'Component';
|
||||
|
||||
// Check if already exists
|
||||
$existing = $modelClass::where('business_id', $business->id)
|
||||
->where('name', $mysqlCategory['name'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
echo "⊘ Skipping '{$mysqlCategory['name']}' ({$typeName}, already exists)\n";
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$category = $modelClass::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => $mysqlCategory['name'],
|
||||
'description' => $mysqlCategory['description'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
echo "✓ Imported '{$category->name}' ({$typeName}, ID: {$category->id})\n";
|
||||
$imported++;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "✗ Error importing '{$mysqlCategory['name']}': {$e->getMessage()}\n";
|
||||
$errors[] = [
|
||||
'category' => $mysqlCategory['name'],
|
||||
'type' => $typeName,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n================\n";
|
||||
echo "IMPORT COMPLETE\n";
|
||||
echo "================\n\n";
|
||||
|
||||
echo "Summary:\n";
|
||||
echo " Total categories in file: " . count($mysqlCategories) . "\n";
|
||||
echo " Successfully imported: {$imported}\n";
|
||||
echo " Skipped (already exists): {$skipped}\n";
|
||||
echo " Errors: " . count($errors) . "\n\n";
|
||||
|
||||
if (count($errors) > 0) {
|
||||
echo "Errors:\n";
|
||||
foreach ($errors as $error) {
|
||||
echo " - {$error['category']}: {$error['error']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Show breakdown by type
|
||||
$productCount = ProductCategory::where('business_id', $business->id)->count();
|
||||
$componentCount = ComponentCategory::where('business_id', $business->id)->count();
|
||||
|
||||
echo "Category breakdown:\n";
|
||||
echo " Product categories: {$productCount}\n";
|
||||
echo " Component categories: {$componentCount}\n";
|
||||
echo " Total: " . ($productCount + $componentCount) . "\n\n";
|
||||
|
||||
echo "Next steps:\n";
|
||||
echo "1. Review categories at: /s/{$business->slug}/categories\n";
|
||||
echo "2. Create/import products and components using these categories\n";
|
||||
echo "3. Verify category assignments are correct\n\n";
|
||||
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>
|
||||
@@ -351,11 +351,16 @@
|
||||
</div>
|
||||
<div class="from-base-100/60 pointer-events-none absolute start-0 end-0 bottom-0 h-7 bg-gradient-to-t to-transparent"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="sticky bottom-0 bg-base-100 pb-2 z-10">
|
||||
<hr class="border-base-300 my-2 border-dashed" />
|
||||
|
||||
<!-- User Account Section -->
|
||||
<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">
|
||||
<div class="mx-2 px-3 text-xs text-center text-base-content/50">
|
||||
<p class="mb-0.5">{{ config('version.company.name') }} hub</p>
|
||||
<p class="font-mono mb-0.5">
|
||||
@if($appVersion === 'dev')
|
||||
@@ -364,54 +369,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.com, LLC</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
76
resources/views/components/view-switcher.blade.php
Normal file
76
resources/views/components/view-switcher.blade.php
Normal file
@@ -0,0 +1,76 @@
|
||||
@php
|
||||
$currentBusiness = auth()->user()?->primaryBusiness();
|
||||
$currentView = session('current_view', 'sales');
|
||||
|
||||
$views = [
|
||||
'sales' => [
|
||||
'name' => 'Sales',
|
||||
'icon' => 'icon-[lucide--shopping-cart]',
|
||||
'enabled' => true,
|
||||
],
|
||||
'manufacturing' => [
|
||||
'name' => 'Manufacturing',
|
||||
'icon' => 'icon-[lucide--factory]',
|
||||
'enabled' => $currentBusiness?->has_manufacturing ?? false,
|
||||
],
|
||||
'compliance' => [
|
||||
'name' => 'Compliance',
|
||||
'icon' => 'icon-[lucide--shield-check]',
|
||||
'enabled' => $currentBusiness?->has_compliance ?? false,
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="mb-3 px-2.5 sticky top-0 bg-base-100 z-10 py-2" x-data="{ open: false }">
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="open = !open"
|
||||
@click.away="open = false"
|
||||
type="button"
|
||||
class="bg-base-200 hover:bg-base-300 rounded-box w-full flex items-center justify-between gap-2 px-3 py-2.5 transition-all">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="{{ $views[$currentView]['icon'] }} size-4 flex-shrink-0"></span>
|
||||
<span class="text-sm font-semibold truncate max-w-[140px]">
|
||||
{{ $views[$currentView]['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="icon-[lucide--chevron-down] size-4 flex-shrink-0 transition-transform" :class="{ 'rotate-180': open }"></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="absolute z-50 mt-1 w-full bg-base-100 rounded-box shadow-xl border border-base-300"
|
||||
style="display: none;">
|
||||
|
||||
@foreach($views as $viewKey => $viewData)
|
||||
@if($viewData['enabled'])
|
||||
<form action="{{ route('seller.business.view.switch', $currentBusiness->slug) }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="view" value="{{ $viewKey }}">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-3 py-2 text-left hover:bg-base-200 transition-colors flex items-center gap-2 {{ $currentView === $viewKey ? 'bg-primary/10 text-primary font-semibold' : '' }}">
|
||||
<span class="{{ $viewData['icon'] }} size-4"></span>
|
||||
<span class="truncate">{{ $viewData['name'] }}</span>
|
||||
@if($currentView === $viewKey)
|
||||
<span class="icon-[lucide--check] size-4 ml-auto"></span>
|
||||
@endif
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div class="w-full px-3 py-2 flex items-center gap-2 opacity-50 cursor-not-allowed">
|
||||
<span class="{{ $viewData['icon'] }} size-4"></span>
|
||||
<span class="truncate">{{ $viewData['name'] }}</span>
|
||||
<span class="badge badge-sm badge-outline ml-auto">Upgrade</span>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
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
|
||||
@@ -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
|
||||
@@ -35,6 +35,11 @@ Route::bind('invoice', function (string $value) {
|
||||
->firstOrFail();
|
||||
});
|
||||
|
||||
// Custom route model binding for users by UUID
|
||||
Route::bind('user', function (string $value) {
|
||||
return \App\Models\User::where('uuid', $value)->firstOrFail();
|
||||
});
|
||||
|
||||
// Seller-specific routes under /s/ prefix (moved from /b/)
|
||||
Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
||||
// Root redirect to dashboard
|
||||
@@ -268,6 +273,9 @@ Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
||||
// Examples: overview, sales-analytics, product-analytics, customer-analytics, manufacturing-analytics, marketing-analytics
|
||||
});
|
||||
|
||||
// View Switcher (business-scoped but outside settings group)
|
||||
Route::post('/view/switch', [\App\Http\Controllers\Seller\SettingsController::class, 'switchView'])->name('view.switch');
|
||||
|
||||
// Settings Module
|
||||
// Always enabled, controlled by role-based permissions
|
||||
// Features: Business configuration, user management, billing
|
||||
@@ -275,14 +283,45 @@ Route::prefix('s')->name('seller.')->middleware('seller')->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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user