Compare commits
10 Commits
docs/add-f
...
working-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5068d13d | ||
|
|
84f364de74 | ||
|
|
39c955cdc4 | ||
|
|
e02ca54187 | ||
|
|
ac46ee004b | ||
|
|
17a6eb260d | ||
|
|
5ea80366be | ||
|
|
99aa0cb980 | ||
|
|
3de53a76d0 | ||
|
|
7fa9b6aff8 |
120
CLAUDE.local.md
Normal file
120
CLAUDE.local.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Local Workflow Notes (NOT COMMITTED TO GIT)
|
||||
|
||||
## 🚨 MANDATORY WORKTREES WORKFLOW
|
||||
|
||||
**CRITICAL:** Claude MUST use worktrees for ALL feature work. NO exceptions.
|
||||
|
||||
### Worktrees Location
|
||||
- **Main repo:** `C:\Users\Boss Man\Documents\GitHub\hub`
|
||||
- **Worktrees folder:** `C:\Users\Boss Man\Documents\GitHub\Work Trees\`
|
||||
|
||||
---
|
||||
|
||||
## Claude's Proactive Workflow Guide
|
||||
|
||||
### When User Requests a Feature
|
||||
|
||||
**Claude MUST immediately say:**
|
||||
> "Let me create a worktree for this feature"
|
||||
|
||||
**Then Claude MUST:**
|
||||
|
||||
1. **Create worktree with descriptive name:**
|
||||
```bash
|
||||
cd "C:\Users\Boss Man\Documents\GitHub\hub"
|
||||
git worktree add "../Work Trees/feature-descriptive-name" -b feature/descriptive-name
|
||||
```
|
||||
|
||||
2. **Switch to worktree:**
|
||||
```bash
|
||||
cd "C:\Users\Boss Man\Documents\GitHub\Work Trees/feature-descriptive-name"
|
||||
```
|
||||
|
||||
3. **Work on that ONE feature only:**
|
||||
- Keep focused on single feature
|
||||
- Commit regularly with clear messages
|
||||
- Run tests frequently
|
||||
|
||||
### When Feature is Complete
|
||||
|
||||
**Claude MUST prompt user:**
|
||||
> "Feature complete! Ready to create a PR?"
|
||||
|
||||
**Then Claude MUST:**
|
||||
|
||||
1. **Run tests first:**
|
||||
```bash
|
||||
./vendor/bin/pint
|
||||
php artisan test --parallel
|
||||
```
|
||||
|
||||
2. **Push branch:**
|
||||
```bash
|
||||
git push -u origin feature/descriptive-name
|
||||
```
|
||||
|
||||
3. **Create PR with good description:**
|
||||
```bash
|
||||
gh pr create --title "Feature: description" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
- Bullet point summary
|
||||
|
||||
## Changes
|
||||
- What was added/modified
|
||||
|
||||
## Test Plan
|
||||
- How to test
|
||||
|
||||
🤖 Generated with Claude Code
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### After PR is Merged
|
||||
|
||||
**Claude MUST help cleanup:**
|
||||
|
||||
1. **Return to main repo:**
|
||||
```bash
|
||||
cd "C:\Users\Boss Man\Documents\GitHub\hub"
|
||||
```
|
||||
|
||||
2. **Remove worktree:**
|
||||
```bash
|
||||
git worktree remove "../Work Trees/feature-descriptive-name"
|
||||
```
|
||||
|
||||
3. **Delete local branch:**
|
||||
```bash
|
||||
git branch -d feature/descriptive-name
|
||||
```
|
||||
|
||||
4. **Pull latest develop:**
|
||||
```bash
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Worktrees are Mandatory
|
||||
|
||||
- ✅ **Isolation:** Each feature has its own directory
|
||||
- ✅ **No conflicts:** Work on multiple features safely
|
||||
- ✅ **Clean commits:** No mixing of changes
|
||||
- ✅ **Safety:** Main repo stays clean on develop
|
||||
- ✅ **Easy PR workflow:** One worktree = one PR
|
||||
|
||||
---
|
||||
|
||||
## Emergency: Uncommitted Work in Main Repo
|
||||
|
||||
If there are uncommitted changes in main repo:
|
||||
|
||||
1. **Best:** Commit to feature branch first
|
||||
2. **Alternative:** Stash them: `git stash`
|
||||
3. **Last resort:** Ask user what to do
|
||||
|
||||
---
|
||||
|
||||
**Note:** This file is in `.gitignore` and will never be committed or pushed to remote.
|
||||
157
CONTRIBUTING.md
157
CONTRIBUTING.md
@@ -239,6 +239,163 @@ git push origin feature/my-feature
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
### Keeping Your Feature Branch Up-to-Date
|
||||
|
||||
**Best practice for teams:** Sync your feature branch with `develop` regularly to avoid large merge conflicts.
|
||||
|
||||
#### Daily Start-of-Work Routine
|
||||
|
||||
```bash
|
||||
# 1. Get latest changes from develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
# 2. Update your feature branch
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
|
||||
# 3. If there are conflicts (see below), resolve them
|
||||
# 4. Continue working
|
||||
```
|
||||
|
||||
**How often?**
|
||||
- Minimum: Once per day (start of work)
|
||||
- Better: Multiple times per day if develop is active
|
||||
- Always: Before creating your Pull Request
|
||||
|
||||
#### Merge vs Rebase: Which to Use?
|
||||
|
||||
**For teams of 5+ developers, use `merge` (not `rebase`):**
|
||||
|
||||
```bash
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
```
|
||||
|
||||
**Why merge over rebase?**
|
||||
- ✅ Safer: Preserves your commit history
|
||||
- ✅ Collaborative: Works when multiple people work on the same feature branch
|
||||
- ✅ Transparent: Shows when you integrated upstream changes
|
||||
- ✅ No force-push: Once you've pushed to origin, merge won't require `--force`
|
||||
|
||||
**When to use rebase:**
|
||||
- ⚠️ Only if you haven't pushed yet
|
||||
- ⚠️ Only if you're the sole developer on the branch
|
||||
- ⚠️ You want a cleaner, linear history
|
||||
|
||||
```bash
|
||||
# Only do this if you haven't pushed yet!
|
||||
git checkout feature/my-feature
|
||||
git rebase develop
|
||||
```
|
||||
|
||||
**Never rebase after pushing** - it rewrites history and breaks collaboration.
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
When you run `git merge develop` and see conflicts:
|
||||
|
||||
```bash
|
||||
$ git merge develop
|
||||
Auto-merging app/Http/Controllers/OrderController.php
|
||||
CONFLICT (content): Merge conflict in app/Http/Controllers/OrderController.php
|
||||
Automatic merge failed; fix conflicts and then commit the result.
|
||||
```
|
||||
|
||||
**Step-by-step resolution:**
|
||||
|
||||
1. **See which files have conflicts:**
|
||||
```bash
|
||||
git status
|
||||
# Look for "both modified:" files
|
||||
```
|
||||
|
||||
2. **Open conflicted files** - look for conflict markers:
|
||||
```php
|
||||
<<<<<<< HEAD
|
||||
// Your code
|
||||
=======
|
||||
// Code from develop
|
||||
>>>>>>> develop
|
||||
```
|
||||
|
||||
3. **Resolve conflicts** - edit the file to keep what you need:
|
||||
```php
|
||||
// Choose your code, their code, or combine both
|
||||
// Remove the <<<, ===, >>> markers
|
||||
```
|
||||
|
||||
4. **Mark as resolved:**
|
||||
```bash
|
||||
git add app/Http/Controllers/OrderController.php
|
||||
```
|
||||
|
||||
5. **Complete the merge:**
|
||||
```bash
|
||||
git commit -m "merge: resolve conflicts with develop"
|
||||
```
|
||||
|
||||
6. **Run tests to ensure nothing broke:**
|
||||
```bash
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
7. **Push the merge commit:**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
#### When Conflicts Are Too Complex
|
||||
|
||||
If conflicts are extensive or you're unsure:
|
||||
|
||||
1. **Abort the merge:**
|
||||
```bash
|
||||
git merge --abort
|
||||
```
|
||||
|
||||
2. **Ask for help** in #engineering Slack:
|
||||
- "I'm merging develop into feature/X and have conflicts in OrderController"
|
||||
- Someone might have context on the upstream changes
|
||||
|
||||
3. **Pair program the resolution** - screen share with the person who made the conflicting changes
|
||||
|
||||
4. **Alternative: Start fresh** (last resort):
|
||||
```bash
|
||||
# Create new branch from latest develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/my-feature-v2
|
||||
|
||||
# Cherry-pick your commits
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
#### Example: Multi-Day Feature Work
|
||||
|
||||
```bash
|
||||
# Monday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Get latest changes
|
||||
# Work all day, make commits
|
||||
|
||||
# Tuesday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Sync again (someone added auth changes)
|
||||
# Continue working
|
||||
|
||||
# Wednesday
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Final sync before PR
|
||||
git push origin feature/payment-integration
|
||||
# Create Pull Request
|
||||
```
|
||||
|
||||
**Result:** Small, manageable syncs instead of one huge conflict on PR day.
|
||||
|
||||
### When to Test Locally
|
||||
|
||||
**Always run tests before pushing if you:**
|
||||
|
||||
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
|
||||
364
app/Http/Controllers/Seller/BatchController.php
Normal file
364
app/Http/Controllers/Seller/BatchController.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Seller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Batch;
|
||||
use App\Models\Business;
|
||||
use App\Models\Product;
|
||||
use App\Services\QrCodeService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class BatchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of batches for the business
|
||||
*/
|
||||
public function index(Request $request, Business $business)
|
||||
{
|
||||
// Build query for batches
|
||||
$query = Batch::where('business_id', $business->id)
|
||||
->with(['product.brand', 'coaFiles'])
|
||||
->orderBy('production_date', 'desc');
|
||||
|
||||
// Search filter
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('batch_number', 'LIKE', "%{$search}%")
|
||||
->orWhere('test_id', 'LIKE', "%{$search}%")
|
||||
->orWhere('lot_number', 'LIKE', "%{$search}%")
|
||||
->orWhereHas('product', function ($productQuery) use ($search) {
|
||||
$productQuery->where('name', 'LIKE', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$batches = $query->paginate(20)->withQueryString();
|
||||
|
||||
// Separate active and inactive batches
|
||||
$activeBatches = $batches->filter(fn ($batch) => $batch->is_active);
|
||||
$inactiveBatches = $batches->filter(fn ($batch) => ! $batch->is_active);
|
||||
|
||||
return view('seller.batches.index', compact('business', 'batches', 'activeBatches', 'inactiveBatches'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new batch
|
||||
*/
|
||||
public function create(Request $request, Business $business)
|
||||
{
|
||||
// Get products owned by this business
|
||||
$products = Product::whereHas('brand', function ($query) use ($business) {
|
||||
$query->where('business_id', $business->id);
|
||||
})->orderBy('name', 'asc')->get();
|
||||
|
||||
// For the new architecture, components are products (the view expects $components)
|
||||
$components = $products;
|
||||
|
||||
// Get existing component batches that can be used as sources for homogenized batches
|
||||
$componentBatches = Batch::where('business_id', $business->id)
|
||||
->where('quantity_remaining', '>', 0)
|
||||
->where('is_active', true)
|
||||
->where('is_quarantined', false)
|
||||
->with('component')
|
||||
->orderBy('batch_number')
|
||||
->get();
|
||||
|
||||
return view('seller.batches.create', compact('business', 'products', 'components', 'componentBatches'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created batch
|
||||
*/
|
||||
public function store(Request $request, Business $business)
|
||||
{
|
||||
// Determine max value based on unit (% vs mg/g, mg/ml, mg/unit)
|
||||
$maxValue = $request->cannabinoid_unit === '%' ? 100 : 1000;
|
||||
|
||||
$validated = $request->validate([
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'cannabinoid_unit' => 'required|string|in:%,MG/ML,MG/G,MG/UNIT',
|
||||
'batch_number' => 'nullable|string|max:100|unique:batches,batch_number',
|
||||
'production_date' => 'nullable|date',
|
||||
'test_date' => 'nullable|date',
|
||||
'test_id' => 'nullable|string|max:100',
|
||||
'lot_number' => 'nullable|string|max:100',
|
||||
'lab_name' => 'nullable|string|max:255',
|
||||
'thc_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'thca_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbd_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbda_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbg_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbn_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'delta_9_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'total_terps_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'notes' => 'nullable|string',
|
||||
'coa_files.*' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240', // 10MB max per file
|
||||
]);
|
||||
|
||||
// Verify product belongs to this business
|
||||
$product = Product::whereHas('brand', function ($query) use ($business) {
|
||||
$query->where('business_id', $business->id);
|
||||
})->findOrFail($validated['product_id']);
|
||||
|
||||
// Set business_id
|
||||
$validated['business_id'] = $business->id;
|
||||
$validated['is_active'] = true; // New batches are active by default
|
||||
|
||||
// Create batch (calculations happen in model boot method)
|
||||
$batch = Batch::create($validated);
|
||||
|
||||
// Handle COA file uploads
|
||||
if ($request->hasFile('coa_files')) {
|
||||
foreach ($request->file('coa_files') as $index => $file) {
|
||||
$storagePath = "businesses/{$business->uuid}/batches/{$batch->id}/coas";
|
||||
$fileName = uniqid().'.'.$file->getClientOriginalExtension();
|
||||
$filePath = $file->storeAs($storagePath, $fileName, 'public');
|
||||
|
||||
$batch->coaFiles()->create([
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
'file_path' => $filePath,
|
||||
'file_type' => $file->getClientOriginalExtension(),
|
||||
'file_size' => $file->getSize(),
|
||||
'is_primary' => $index === 0,
|
||||
'display_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-generate QR code for the new batch (with brand logo if available)
|
||||
$qrService = app(QrCodeService::class);
|
||||
$qrService->generateWithLogo($batch);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.batches.index', $business->slug)
|
||||
->with('success', 'Batch created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified batch
|
||||
*/
|
||||
public function edit(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
// Get products owned by this business
|
||||
$products = Product::whereHas('brand', function ($query) use ($business) {
|
||||
$query->where('business_id', $business->id);
|
||||
})->orderBy('name', 'asc')->get();
|
||||
|
||||
$batch->load('coaFiles');
|
||||
|
||||
return view('seller.batches.edit', compact('business', 'batch', 'products'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified batch
|
||||
*/
|
||||
public function update(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
// Determine max value based on unit (% vs mg/g, mg/ml, mg/unit)
|
||||
$maxValue = $request->cannabinoid_unit === '%' ? 100 : 1000;
|
||||
|
||||
$validated = $request->validate([
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'cannabinoid_unit' => 'required|string|in:%,MG/ML,MG/G,MG/UNIT',
|
||||
'batch_number' => 'nullable|string|max:100|unique:batches,batch_number,'.$batch->id,
|
||||
'production_date' => 'nullable|date',
|
||||
'test_date' => 'nullable|date',
|
||||
'test_id' => 'nullable|string|max:100',
|
||||
'lot_number' => 'nullable|string|max:100',
|
||||
'lab_name' => 'nullable|string|max:255',
|
||||
'thc_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'thca_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbd_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbda_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbg_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'cbn_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'delta_9_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'total_terps_percentage' => "nullable|numeric|min:0|max:{$maxValue}",
|
||||
'notes' => 'nullable|string',
|
||||
'coa_files.*' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240', // 10MB max per file
|
||||
]);
|
||||
|
||||
// Verify product belongs to this business
|
||||
$product = Product::whereHas('brand', function ($query) use ($business) {
|
||||
$query->where('business_id', $business->id);
|
||||
})->findOrFail($validated['product_id']);
|
||||
|
||||
// Update batch (calculations happen in model boot method)
|
||||
$batch->update($validated);
|
||||
|
||||
// Handle new COA file uploads
|
||||
if ($request->hasFile('coa_files')) {
|
||||
$existingFilesCount = $batch->coaFiles()->count();
|
||||
foreach ($request->file('coa_files') as $index => $file) {
|
||||
$storagePath = "businesses/{$business->uuid}/batches/{$batch->id}/coas";
|
||||
$fileName = uniqid().'.'.$file->getClientOriginalExtension();
|
||||
$filePath = $file->storeAs($storagePath, $fileName, 'public');
|
||||
|
||||
$batch->coaFiles()->create([
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
'file_path' => $filePath,
|
||||
'file_type' => $file->getClientOriginalExtension(),
|
||||
'file_size' => $file->getSize(),
|
||||
'is_primary' => $existingFilesCount === 0 && $index === 0,
|
||||
'display_order' => $existingFilesCount + $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.batches.index', $business->slug)
|
||||
->with('success', 'Batch updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified batch
|
||||
*/
|
||||
public function destroy(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
// Delete associated COA files from storage
|
||||
foreach ($batch->coaFiles as $coaFile) {
|
||||
if (Storage::disk('public')->exists($coaFile->file_path)) {
|
||||
Storage::disk('public')->delete($coaFile->file_path);
|
||||
}
|
||||
}
|
||||
|
||||
$batch->delete();
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.batches.index', $business->slug)
|
||||
->with('success', 'Batch deleted successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR code for a batch
|
||||
*/
|
||||
public function generateQrCode(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->generateWithLogo($batch);
|
||||
|
||||
// Refresh batch to get updated qr_code_path
|
||||
$batch->refresh();
|
||||
|
||||
return response()->json([
|
||||
'success' => $result['success'],
|
||||
'message' => $result['message'],
|
||||
'qr_code_url' => $batch->qr_code_path ? Storage::url($batch->qr_code_path) : null,
|
||||
'download_url' => $batch->qr_code_path ? route('seller.business.batches.qr-code.download', [$business->slug, $batch->id]) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download QR code for a batch
|
||||
*/
|
||||
public function downloadQrCode(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$qrService = app(QrCodeService::class);
|
||||
$download = $qrService->download($batch);
|
||||
|
||||
if (! $download) {
|
||||
return back()->with('error', 'QR code not found');
|
||||
}
|
||||
|
||||
return $download;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate QR code for a batch
|
||||
*/
|
||||
public function regenerateQrCode(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->regenerate($batch);
|
||||
|
||||
// Refresh batch to get updated qr_code_path
|
||||
$batch->refresh();
|
||||
|
||||
return response()->json([
|
||||
'success' => $result['success'],
|
||||
'message' => $result['message'],
|
||||
'qr_code_url' => $batch->qr_code_path ? Storage::url($batch->qr_code_path) : null,
|
||||
'download_url' => $batch->qr_code_path ? route('seller.business.batches.qr-code.download', [$business->slug, $batch->id]) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete QR code for a batch
|
||||
*/
|
||||
public function deleteQrCode(Request $request, Business $business, Batch $batch)
|
||||
{
|
||||
// Verify batch belongs to this business
|
||||
if ($batch->business_id !== $business->id) {
|
||||
abort(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->delete($batch);
|
||||
|
||||
return response()->json([
|
||||
'success' => $result['success'],
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk generate QR codes for multiple batches
|
||||
*/
|
||||
public function bulkGenerateQrCodes(Request $request, Business $business)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'batch_ids' => 'required|array',
|
||||
'batch_ids.*' => 'exists:batches,id',
|
||||
]);
|
||||
|
||||
// Verify all batches belong to this business
|
||||
$batches = Batch::whereIn('id', $validated['batch_ids'])
|
||||
->where('business_id', $business->id)
|
||||
->get();
|
||||
|
||||
if ($batches->count() !== count($validated['batch_ids'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Some batches do not belong to this business',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$qrService = app(QrCodeService::class);
|
||||
$result = $qrService->bulkGenerate($validated['batch_ids']);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
256
app/Http/Controllers/Seller/BrandController.php
Normal file
256
app/Http/Controllers/Seller/BrandController.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Seller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BrandController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of brands for the business
|
||||
*/
|
||||
public function index(Request $request, Business $business)
|
||||
{
|
||||
$brands = $business->brands()
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.brands.index', compact('business', 'brands'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new brand
|
||||
*/
|
||||
public function create(Business $business)
|
||||
{
|
||||
return view('seller.brands.create', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created brand in storage
|
||||
*/
|
||||
public function store(Request $request, Business $business)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'tagline' => 'nullable|string|max:45',
|
||||
'description' => 'nullable|string|max:300',
|
||||
'long_description' => 'nullable|string|max:1000',
|
||||
'website_url' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'unit_number' => 'nullable|string|max:50',
|
||||
'city' => 'nullable|string|max:100',
|
||||
'state' => 'nullable|string|max:2',
|
||||
'zip_code' => 'nullable|string|max:10',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
'banner' => 'nullable|image|max:4096',
|
||||
'is_public' => 'boolean',
|
||||
'is_featured' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'instagram_handle' => 'nullable|string|max:255',
|
||||
'facebook_url' => 'nullable|url|max:255',
|
||||
'twitter_handle' => 'nullable|string|max:255',
|
||||
'youtube_url' => 'nullable|url|max:255',
|
||||
]);
|
||||
|
||||
// Automatically add https:// to website_url if not present
|
||||
if ($request->filled('website_url')) {
|
||||
$url = $validated['website_url'];
|
||||
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||
$validated['website_url'] = 'https://' . $url;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
|
||||
// Handle logo upload
|
||||
if ($request->hasFile('logo')) {
|
||||
$validated['logo_path'] = $request->file('logo')->store('brands/logos', 'public');
|
||||
}
|
||||
|
||||
// Handle banner upload
|
||||
if ($request->hasFile('banner')) {
|
||||
$validated['banner_path'] = $request->file('banner')->store('brands/banners', 'public');
|
||||
}
|
||||
|
||||
// Set boolean defaults
|
||||
$validated['is_public'] = $request->boolean('is_public');
|
||||
$validated['is_featured'] = $request->boolean('is_featured');
|
||||
$validated['is_active'] = $request->boolean('is_active');
|
||||
|
||||
// Create brand
|
||||
$brand = $business->brands()->create($validated);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.brands.index', $business->slug)
|
||||
->with('success', 'Brand created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified brand
|
||||
*/
|
||||
public function show(Business $business, Brand $brand)
|
||||
{
|
||||
// Ensure brand belongs to this business
|
||||
if ($brand->business_id !== $business->id) {
|
||||
abort(403, 'This brand does not belong to your business.');
|
||||
}
|
||||
|
||||
return view('seller.brands.show', compact('business', 'brand'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the specified brand
|
||||
*/
|
||||
public function preview(Business $business, Brand $brand)
|
||||
{
|
||||
// Ensure brand belongs to this business
|
||||
if ($brand->business_id !== $business->id) {
|
||||
abort(403, 'This brand does not belong to your business.');
|
||||
}
|
||||
|
||||
return view('seller.brands.preview', compact('business', 'brand'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified brand
|
||||
*/
|
||||
public function edit(Business $business, Brand $brand)
|
||||
{
|
||||
// Ensure brand belongs to this business
|
||||
if ($brand->business_id !== $business->id) {
|
||||
abort(403, 'This brand does not belong to your business.');
|
||||
}
|
||||
|
||||
return view('seller.brands.edit', compact('business', 'brand'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified brand in storage
|
||||
*/
|
||||
public function update(Request $request, Business $business, Brand $brand)
|
||||
{
|
||||
// Ensure brand belongs to this business
|
||||
if ($brand->business_id !== $business->id) {
|
||||
abort(403, 'This brand does not belong to your business.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'tagline' => 'nullable|string|max:45',
|
||||
'description' => 'nullable|string|max:300',
|
||||
'long_description' => 'nullable|string|max:1000',
|
||||
'website_url' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'unit_number' => 'nullable|string|max:50',
|
||||
'city' => 'nullable|string|max:100',
|
||||
'state' => 'nullable|string|max:2',
|
||||
'zip_code' => 'nullable|string|max:10',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
'banner' => 'nullable|image|max:4096',
|
||||
'remove_logo' => 'boolean',
|
||||
'remove_banner' => 'boolean',
|
||||
'is_public' => 'boolean',
|
||||
'is_featured' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'instagram_handle' => 'nullable|string|max:255',
|
||||
'facebook_url' => 'nullable|url|max:255',
|
||||
'twitter_handle' => 'nullable|string|max:255',
|
||||
'youtube_url' => 'nullable|url|max:255',
|
||||
]);
|
||||
|
||||
// Automatically add https:// to website_url if not present
|
||||
if ($request->filled('website_url')) {
|
||||
$url = $validated['website_url'];
|
||||
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||
$validated['website_url'] = 'https://' . $url;
|
||||
}
|
||||
} else {
|
||||
$validated['website_url'] = null;
|
||||
}
|
||||
|
||||
// Update slug if name changed
|
||||
if ($validated['name'] !== $brand->name) {
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
}
|
||||
|
||||
// Handle logo removal
|
||||
if ($request->boolean('remove_logo') && $brand->logo_path) {
|
||||
Storage::disk('public')->delete($brand->logo_path);
|
||||
$validated['logo_path'] = null;
|
||||
}
|
||||
|
||||
// Handle logo upload
|
||||
if ($request->hasFile('logo')) {
|
||||
// Delete old logo
|
||||
if ($brand->logo_path) {
|
||||
Storage::disk('public')->delete($brand->logo_path);
|
||||
}
|
||||
$validated['logo_path'] = $request->file('logo')->store('brands/logos', 'public');
|
||||
}
|
||||
|
||||
// Handle banner removal
|
||||
if ($request->boolean('remove_banner') && $brand->banner_path) {
|
||||
Storage::disk('public')->delete($brand->banner_path);
|
||||
$validated['banner_path'] = null;
|
||||
}
|
||||
|
||||
// Handle banner upload
|
||||
if ($request->hasFile('banner')) {
|
||||
// Delete old banner
|
||||
if ($brand->banner_path) {
|
||||
Storage::disk('public')->delete($brand->banner_path);
|
||||
}
|
||||
$validated['banner_path'] = $request->file('banner')->store('brands/banners', 'public');
|
||||
}
|
||||
|
||||
// Set boolean defaults
|
||||
$validated['is_public'] = $request->boolean('is_public');
|
||||
$validated['is_featured'] = $request->boolean('is_featured');
|
||||
$validated['is_active'] = $request->boolean('is_active');
|
||||
|
||||
// Remove form-only fields
|
||||
unset($validated['remove_logo'], $validated['remove_banner']);
|
||||
|
||||
// Update brand
|
||||
$brand->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.brands.index', $business->slug)
|
||||
->with('success', 'Brand updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified brand from storage
|
||||
*/
|
||||
public function destroy(Business $business, Brand $brand)
|
||||
{
|
||||
// Ensure brand belongs to this business
|
||||
if ($brand->business_id !== $business->id) {
|
||||
abort(403, 'This brand does not belong to your business.');
|
||||
}
|
||||
|
||||
// Delete logo and banner files
|
||||
if ($brand->logo_path) {
|
||||
Storage::disk('public')->delete($brand->logo_path);
|
||||
}
|
||||
if ($brand->banner_path) {
|
||||
Storage::disk('public')->delete($brand->banner_path);
|
||||
}
|
||||
|
||||
$brand->delete();
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.brands.index', $business->slug)
|
||||
->with('success', 'Brand deleted successfully!');
|
||||
}
|
||||
}
|
||||
224
app/Http/Controllers/Seller/CategoryController.php
Normal file
224
app/Http/Controllers/Seller/CategoryController.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Seller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Business;
|
||||
use App\Models\ComponentCategory;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(Business $business)
|
||||
{
|
||||
// Load product categories with nesting and counts
|
||||
$productCategories = ProductCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => function ($query) {
|
||||
$query->orderBy('sort_order')->orderBy('name');
|
||||
}])
|
||||
->withCount('products')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Load component categories with nesting and counts
|
||||
$componentCategories = ComponentCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => function ($query) {
|
||||
$query->orderBy('sort_order')->orderBy('name');
|
||||
}])
|
||||
->withCount('components')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.index', compact('business', 'productCategories', 'componentCategories'));
|
||||
}
|
||||
|
||||
public function create(Business $business, string $type)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Get all categories of this type for parent selection
|
||||
$categories = $type === 'product'
|
||||
? ProductCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
: ComponentCategory::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.create', compact('business', 'type', 'categories'));
|
||||
}
|
||||
|
||||
public function store(Request $request, Business $business, string $type)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$tableName = $type === 'product' ? 'product_categories' : 'component_categories';
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => "nullable|exists:{$tableName},id",
|
||||
'description' => 'nullable|string',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'image' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$validated['business_id'] = $business->id;
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
||||
|
||||
// Handle image upload
|
||||
if ($request->hasFile('image')) {
|
||||
$validated['image_path'] = $request->file('image')->store('categories', 'public');
|
||||
}
|
||||
|
||||
// Validate parent belongs to same business if provided
|
||||
if (! empty($validated['parent_id'])) {
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$parent = $model::where('business_id', $business->id)->find($validated['parent_id']);
|
||||
|
||||
if (! $parent) {
|
||||
return back()->withErrors(['parent_id' => 'Invalid parent category'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$model::create($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category created successfully');
|
||||
}
|
||||
|
||||
public function edit(Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
// Get all categories of this type for parent selection (excluding self and descendants)
|
||||
$categories = $model::where('business_id', $business->id)
|
||||
->whereNull('parent_id')
|
||||
->where('id', '!=', $id)
|
||||
->with('children')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('seller.settings.categories.edit', compact('business', 'type', 'category', 'categories'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
$tableName = $type === 'product' ? 'product_categories' : 'component_categories';
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => "nullable|exists:{$tableName},id",
|
||||
'description' => 'nullable|string',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'image' => 'nullable|image|max:2048',
|
||||
]);
|
||||
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
$validated['is_active'] = $request->has('is_active') ? true : false;
|
||||
|
||||
// Handle image upload
|
||||
if ($request->hasFile('image')) {
|
||||
// Delete old image if exists
|
||||
if ($category->image_path) {
|
||||
\Storage::disk('public')->delete($category->image_path);
|
||||
}
|
||||
$validated['image_path'] = $request->file('image')->store('categories', 'public');
|
||||
}
|
||||
|
||||
// Validate parent (can't be self or descendant)
|
||||
if (! empty($validated['parent_id'])) {
|
||||
if ($validated['parent_id'] == $id) {
|
||||
return back()->withErrors(['parent_id' => 'Category cannot be its own parent'])->withInput();
|
||||
}
|
||||
|
||||
$parent = $model::where('business_id', $business->id)->find($validated['parent_id']);
|
||||
if (! $parent) {
|
||||
return back()->withErrors(['parent_id' => 'Invalid parent category'])->withInput();
|
||||
}
|
||||
|
||||
// Check for circular reference (if parent's parent is this category)
|
||||
if ($parent->parent_id == $id) {
|
||||
return back()->withErrors(['parent_id' => 'This would create a circular reference'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$category->update($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category updated successfully');
|
||||
}
|
||||
|
||||
public function destroy(Business $business, string $type, int $id)
|
||||
{
|
||||
// Validate type
|
||||
if (! in_array($type, ['product', 'component'])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$model = $type === 'product' ? ProductCategory::class : ComponentCategory::class;
|
||||
$category = $model::where('business_id', $business->id)->findOrFail($id);
|
||||
|
||||
// Check if has products/components
|
||||
if ($type === 'product') {
|
||||
$count = $category->products()->count();
|
||||
if ($count > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$count} products. Please reassign or delete products first.");
|
||||
}
|
||||
} else {
|
||||
$count = $category->components()->count();
|
||||
if ($count > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$count} components. Please reassign or delete components first.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if has children
|
||||
$childCount = $category->children()->count();
|
||||
if ($childCount > 0) {
|
||||
return back()->with('error', "Cannot delete category with {$childCount} subcategories. Please delete or move subcategories first.");
|
||||
}
|
||||
|
||||
// Delete image if exists
|
||||
if ($category->image_path) {
|
||||
\Storage::disk('public')->delete($category->image_path);
|
||||
}
|
||||
|
||||
$category->delete();
|
||||
|
||||
return redirect()->route('seller.business.settings.categories.index', $business->slug)
|
||||
->with('success', ucfirst($type).' category deleted successfully');
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line created successfully.');
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line updated successfully.');
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ProductLineController extends Controller
|
||||
$productLine->delete();
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line deleted successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,89 @@ use Illuminate\Http\Request;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the personal profile page.
|
||||
*/
|
||||
public function profile(Business $business)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// Get login history (assuming a login_histories table exists)
|
||||
$loginHistory = collect(); // Placeholder - will be implemented with login history tracking
|
||||
|
||||
return view('seller.settings.profile', compact('business', 'loginHistory'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the personal profile.
|
||||
*/
|
||||
public function updateProfile(Business $business, Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
|
||||
'avatar' => 'nullable|image|max:2048',
|
||||
'remove_avatar' => 'nullable|boolean',
|
||||
'use_gravatar' => 'nullable|boolean',
|
||||
'linkedin_url' => 'nullable|url|max:255',
|
||||
'twitter_url' => 'nullable|url|max:255',
|
||||
'facebook_url' => 'nullable|url|max:255',
|
||||
'instagram_url' => 'nullable|url|max:255',
|
||||
'github_url' => 'nullable|url|max:255',
|
||||
]);
|
||||
|
||||
// Handle avatar removal
|
||||
if ($request->has('remove_avatar') && $user->avatar_path) {
|
||||
\Storage::disk('public')->delete($user->avatar_path);
|
||||
$validated['avatar_path'] = null;
|
||||
}
|
||||
|
||||
// Handle avatar upload
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Delete old avatar if exists
|
||||
if ($user->avatar_path) {
|
||||
\Storage::disk('public')->delete($user->avatar_path);
|
||||
}
|
||||
|
||||
$path = $request->file('avatar')->store('avatars', 'public');
|
||||
$validated['avatar_path'] = $path;
|
||||
}
|
||||
|
||||
$user->update($validated);
|
||||
|
||||
return redirect()->route('seller.business.settings.profile', $business->slug)
|
||||
->with('success', 'Profile updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function updatePassword(Business $business, Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'current_password' => 'required|current_password',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'logout_other_sessions' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'password' => bcrypt($validated['password']),
|
||||
]);
|
||||
|
||||
// Logout other sessions if requested
|
||||
if ($request->has('logout_other_sessions')) {
|
||||
auth()->logoutOtherDevices($validated['password']);
|
||||
}
|
||||
|
||||
return redirect()->route('seller.business.settings.profile', $business->slug)
|
||||
->with('success', 'Password updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the company information settings page.
|
||||
*/
|
||||
@@ -18,21 +101,12 @@ class SettingsController extends Controller
|
||||
|
||||
/**
|
||||
* Update the company information.
|
||||
* Note: Only business_phone and business_email can be updated due to compliance requirements.
|
||||
*/
|
||||
public function updateCompanyInformation(Business $business, Request $request)
|
||||
{
|
||||
// Only allow updating business phone and email (hybrid approach)
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'dba_name' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'business_type' => 'nullable|string',
|
||||
'tin_ein' => 'nullable|string|max:20',
|
||||
'license_number' => 'nullable|string|max:255',
|
||||
'license_type' => 'nullable|string',
|
||||
'physical_address' => 'nullable|string|max:255',
|
||||
'physical_city' => 'nullable|string|max:100',
|
||||
'physical_state' => 'nullable|string|max:2',
|
||||
'physical_zipcode' => 'nullable|string|max:10',
|
||||
'business_phone' => 'nullable|string|max:20',
|
||||
'business_email' => 'nullable|email|max:255',
|
||||
]);
|
||||
@@ -41,39 +115,233 @@ class SettingsController extends Controller
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.company-information', $business->slug)
|
||||
->with('success', 'Company information updated successfully!');
|
||||
->with('success', 'Contact information updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the users management settings page.
|
||||
*/
|
||||
public function users(Business $business)
|
||||
public function users(Business $business, Request $request, \App\Services\PermissionService $permissionService)
|
||||
{
|
||||
return view('seller.settings.users', compact('business'));
|
||||
// Exclude business owner from the users list
|
||||
$query = $business->users()->where('users.id', '!=', $business->owner_user_id);
|
||||
|
||||
// Search
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by account type (role)
|
||||
if ($request->filled('account_type')) {
|
||||
$query->whereHas('roles', function ($q) use ($request) {
|
||||
$q->where('name', $request->account_type);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by last login date range
|
||||
if ($request->filled('last_login_start')) {
|
||||
$query->where('last_login_at', '>=', $request->last_login_start);
|
||||
}
|
||||
if ($request->filled('last_login_end')) {
|
||||
$query->where('last_login_at', '<=', $request->last_login_end.' 23:59:59');
|
||||
}
|
||||
|
||||
$users = $query
|
||||
->withPivot('contact_type', 'is_primary', 'permissions', 'role', 'role_template')
|
||||
->with('roles')
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->paginate(15);
|
||||
|
||||
$roleTemplates = $permissionService->getRoleTemplates();
|
||||
$permissionCategories = $permissionService->getPermissionsByCategory();
|
||||
|
||||
return view('seller.settings.users', compact('business', 'users', 'roleTemplates', 'permissionCategories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the order settings page.
|
||||
* Show the form for editing a user's permissions.
|
||||
*/
|
||||
public function orders(Business $business)
|
||||
public function editUser(Business $business, \App\Models\User $user, \App\Services\PermissionService $permissionService)
|
||||
{
|
||||
return view('seller.settings.orders', compact('business'));
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Load user with pivot data
|
||||
$user = $business->users()
|
||||
->withPivot('contact_type', 'is_primary', 'permissions', 'role', 'role_template')
|
||||
->with('roles')
|
||||
->where('users.id', $user->id)
|
||||
->first();
|
||||
|
||||
$roleTemplates = $permissionService->getRoleTemplates();
|
||||
$permissionCategories = $permissionService->getPermissionsByCategory();
|
||||
$isOwner = $business->owner_user_id === $user->id;
|
||||
|
||||
return view('seller.settings.users-edit', compact('business', 'user', 'roleTemplates', 'permissionCategories', 'isOwner'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the brands management page.
|
||||
* Store a newly created user invitation.
|
||||
*/
|
||||
public function brands(Business $business)
|
||||
public function inviteUser(Business $business, Request $request)
|
||||
{
|
||||
return view('seller.settings.brands', compact('business'));
|
||||
$validated = $request->validate([
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'position' => 'nullable|string|max:255',
|
||||
'role' => 'required|string|in:company-owner,company-manager,company-user,company-sales,company-accounting,company-manufacturing,company-processing',
|
||||
'is_point_of_contact' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
// Combine first and last name
|
||||
$fullName = trim($validated['first_name'].' '.$validated['last_name']);
|
||||
|
||||
// Create user and associate with business
|
||||
$user = \App\Models\User::create([
|
||||
'name' => $fullName,
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'password' => bcrypt(str()->random(32)), // Temporary password
|
||||
]);
|
||||
|
||||
// Assign role
|
||||
$user->assignRole($validated['role']);
|
||||
|
||||
// Associate with business with additional pivot data
|
||||
$business->users()->attach($user->id, [
|
||||
'role' => $validated['role'],
|
||||
'is_primary' => false,
|
||||
'contact_type' => $request->has('is_point_of_contact') ? 'primary' : null,
|
||||
]);
|
||||
|
||||
// TODO: Send invitation email with password reset link
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users', $business->slug)
|
||||
->with('success', 'User invited successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the payment settings page.
|
||||
* Update user information and permissions.
|
||||
*/
|
||||
public function payments(Business $business)
|
||||
public function updateUser(Business $business, \App\Models\User $user, Request $request)
|
||||
{
|
||||
return view('seller.settings.payments', compact('business'));
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Prevent modifying business owner
|
||||
if ($business->owner_user_id === $user->id) {
|
||||
return redirect()
|
||||
->route('seller.business.settings.users.edit', [$business->slug, $user->id])
|
||||
->with('error', 'Cannot modify business owner permissions.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'position' => 'nullable|string|max:255',
|
||||
'company' => 'nullable|string|max:255',
|
||||
'contact_type' => 'nullable|string|max:255',
|
||||
'role' => 'nullable|string|max:255',
|
||||
'role_template' => 'nullable|string|max:255',
|
||||
'permissions' => 'nullable|array',
|
||||
]);
|
||||
|
||||
// Update user data
|
||||
$user->update([
|
||||
'position' => $validated['position'] ?? null,
|
||||
'company' => $validated['company'] ?? null,
|
||||
]);
|
||||
|
||||
// Update business_user pivot data
|
||||
$business->users()->updateExistingPivot($user->id, [
|
||||
'contact_type' => $validated['contact_type'] ?? null,
|
||||
'role' => $validated['role'] ?? null,
|
||||
'role_template' => $validated['role_template'] ?? null,
|
||||
'permissions' => $validated['permissions'] ?? null,
|
||||
'permissions_updated_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users.edit', [$business->slug, $user->id])
|
||||
->with('success', 'User updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user from business.
|
||||
*/
|
||||
public function removeUser(Business $business, \App\Models\User $user)
|
||||
{
|
||||
// Check if user belongs to this business
|
||||
if (! $business->users()->where('users.id', $user->id)->exists()) {
|
||||
abort(403, 'User does not belong to this business');
|
||||
}
|
||||
|
||||
// Detach user from business
|
||||
$business->users()->detach($user->id);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.users', $business->slug)
|
||||
->with('success', 'User removed successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the sales configuration page (orders + invoices).
|
||||
*/
|
||||
public function salesConfig(Business $business)
|
||||
{
|
||||
return view('seller.settings.sales-config', compact('business'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sales configuration settings (orders + invoices).
|
||||
*/
|
||||
public function updateSalesConfig(Business $business, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
// Order settings
|
||||
'separate_orders_by_brand' => 'nullable|boolean',
|
||||
'auto_increment_order_ids' => 'nullable|boolean',
|
||||
'show_mark_as_paid' => 'nullable|boolean',
|
||||
'display_crm_license_on_orders' => 'nullable|boolean',
|
||||
'order_minimum' => 'nullable|numeric|min:0',
|
||||
'default_shipping_charge' => 'nullable|numeric|min:0',
|
||||
'free_shipping_minimum' => 'nullable|numeric|min:0',
|
||||
'order_disclaimer' => 'nullable|string|max:2000',
|
||||
'order_invoice_footer' => 'nullable|string|max:1000',
|
||||
'prevent_order_editing' => 'required|in:never,after_approval,after_fulfillment,always',
|
||||
'az_require_patient_count' => 'nullable|boolean',
|
||||
'az_require_allotment_verification' => 'nullable|boolean',
|
||||
// Invoice settings
|
||||
'invoice_payable_company_name' => 'nullable|string|max:255',
|
||||
'invoice_payable_address' => 'nullable|string|max:255',
|
||||
'invoice_payable_city' => 'nullable|string|max:100',
|
||||
'invoice_payable_state' => 'nullable|string|max:2',
|
||||
'invoice_payable_zipcode' => 'nullable|string|max:10',
|
||||
]);
|
||||
|
||||
// Convert checkbox values (null means unchecked)
|
||||
$validated['separate_orders_by_brand'] = $request->has('separate_orders_by_brand');
|
||||
$validated['auto_increment_order_ids'] = $request->has('auto_increment_order_ids');
|
||||
$validated['show_mark_as_paid'] = $request->has('show_mark_as_paid');
|
||||
$validated['display_crm_license_on_orders'] = $request->has('display_crm_license_on_orders');
|
||||
$validated['az_require_patient_count'] = $request->has('az_require_patient_count');
|
||||
$validated['az_require_allotment_verification'] = $request->has('az_require_allotment_verification');
|
||||
|
||||
$business->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.settings.sales-config', $business->slug)
|
||||
->with('success', 'Sales configuration updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,12 +352,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');
|
||||
}
|
||||
}
|
||||
|
||||
231
app/Http/Controllers/Seller/WashReportController.php
Normal file
231
app/Http/Controllers/Seller/WashReportController.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Seller;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Batch;
|
||||
use App\Models\Business;
|
||||
use App\Models\Conversion;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class WashReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of wash reports
|
||||
*/
|
||||
public function index(Business $business)
|
||||
{
|
||||
$conversions = Conversion::where('business_id', $business->id)
|
||||
->where('conversion_type', 'hash_wash')
|
||||
->with(['operator', 'inputBatches', 'batchCreated'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('seller.wash-reports.index', compact('business', 'conversions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Stage 1 form (wash parameters)
|
||||
*/
|
||||
public function createStage1(Business $business)
|
||||
{
|
||||
// Get available Fresh Frozen input material batches for this business
|
||||
$inputBatches = Batch::where('business_id', $business->id)
|
||||
->where('quantity_remaining', '>', 0)
|
||||
->where('is_tested', true)
|
||||
->where('is_quarantined', false)
|
||||
->orderBy('batch_number')
|
||||
->get();
|
||||
|
||||
return view('seller.wash-reports.stage1', compact('business', 'inputBatches'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Stage 1 data and redirect to Stage 2
|
||||
*/
|
||||
public function storeStage1(Business $business, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'wash_date' => 'nullable|date',
|
||||
'input_batch_id' => 'nullable|exists:batches,id',
|
||||
'starting_weight' => 'nullable|numeric|min:0',
|
||||
'soak_time_minutes' => 'nullable|integer|min:0',
|
||||
'room_temperature_f' => 'nullable|numeric',
|
||||
'vessel_temperature_f' => 'nullable|numeric',
|
||||
'strain' => 'nullable|string|max:255',
|
||||
'wash_cycles' => 'nullable|array',
|
||||
'wash_cycles.*.cycle' => 'nullable|integer|min:1',
|
||||
'wash_cycles.*.forward_speed' => 'nullable|integer|min:1|max:10',
|
||||
'wash_cycles.*.reverse_speed' => 'nullable|integer|min:1|max:10',
|
||||
'wash_cycles.*.pause' => 'nullable|integer|min:0',
|
||||
'wash_cycles.*.run_time' => 'nullable|integer|min:1',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Verify batch belongs to business (skip validation if testing)
|
||||
$inputBatch = null;
|
||||
if (isset($validated['input_batch_id'])) {
|
||||
$inputBatch = Batch::where('business_id', $business->id)
|
||||
->where('id', $validated['input_batch_id'])
|
||||
->first();
|
||||
|
||||
// Verify sufficient quantity available
|
||||
if ($inputBatch && isset($validated['starting_weight']) && $inputBatch->quantity_remaining < $validated['starting_weight']) {
|
||||
return back()->withErrors([
|
||||
'starting_weight' => 'Insufficient quantity available. Only '.$inputBatch->quantity_remaining.'g available.',
|
||||
])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
// Create conversion with Stage 1 data
|
||||
$strain = !empty($validated['strain']) ? $validated['strain'] : 'Test';
|
||||
$internalName = $strain . ' Hash Wash #' . now()->format('Ymd-His');
|
||||
|
||||
$conversion = Conversion::create([
|
||||
'business_id' => $business->id,
|
||||
'conversion_type' => 'hash_wash',
|
||||
'status' => 'in_progress',
|
||||
'internal_name' => $internalName,
|
||||
'started_at' => !empty($validated['wash_date']) ? $validated['wash_date'] : now(),
|
||||
'operator_user_id' => auth()->id(),
|
||||
'metadata' => [
|
||||
'stage_1' => [
|
||||
'wash_date' => $validated['wash_date'] ?? null,
|
||||
'cultivator' => ($inputBatch && isset($inputBatch->cultivator)) ? $inputBatch->cultivator : 'Unknown',
|
||||
'starting_weight' => isset($validated['starting_weight']) ? (float) $validated['starting_weight'] : 0,
|
||||
'soak_time_minutes' => isset($validated['soak_time_minutes']) ? (int) $validated['soak_time_minutes'] : 0,
|
||||
'room_temperature_f' => isset($validated['room_temperature_f']) ? (float) $validated['room_temperature_f'] : 0,
|
||||
'vessel_temperature_f' => isset($validated['vessel_temperature_f']) ? (float) $validated['vessel_temperature_f'] : 0,
|
||||
'strain' => $strain,
|
||||
'wash_cycles' => $validated['wash_cycles'] ?? [],
|
||||
],
|
||||
],
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
]);
|
||||
|
||||
// Link input batch to conversion (if batch was selected)
|
||||
if ($inputBatch) {
|
||||
$conversion->inputBatches()->attach($inputBatch->id, [
|
||||
'role' => 'input',
|
||||
'quantity_used' => $validated['starting_weight'] ?? 0,
|
||||
'unit' => 'g',
|
||||
]);
|
||||
}
|
||||
|
||||
// Redirect to Stage 2
|
||||
return redirect()->route('seller.business.wash-reports.stage2', [
|
||||
'business' => $business->slug,
|
||||
'conversion' => $conversion->id,
|
||||
])->with('success', 'Stage 1 completed. Now enter yield details.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Stage 2 form (yield tracking)
|
||||
*/
|
||||
public function createStage2(Business $business, Conversion $conversion)
|
||||
{
|
||||
// Verify conversion belongs to business
|
||||
if ($conversion->business_id !== $business->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Verify Stage 1 is complete
|
||||
if (! $conversion->getStage1Data()) {
|
||||
return redirect()->route('seller.business.wash-reports.stage1', $business->slug)
|
||||
->withErrors(['error' => 'Please complete Stage 1 first.']);
|
||||
}
|
||||
|
||||
$stage1Data = $conversion->getStage1Data();
|
||||
|
||||
return view('seller.wash-reports.stage2', compact('business', 'conversion', 'stage1Data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Stage 2 data and complete conversion
|
||||
*/
|
||||
public function storeStage2(Business $business, Conversion $conversion, Request $request)
|
||||
{
|
||||
// Verify conversion belongs to business
|
||||
if ($conversion->business_id !== $business->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'fresh_press_120u' => 'required|numeric|min:0',
|
||||
'cold_cure_90u' => 'required|numeric|min:0',
|
||||
'rosin_45u' => 'required|numeric|min:0',
|
||||
'green_blonde_160u' => 'required|numeric|min:0',
|
||||
'green_blonde_25u' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$stage1Data = $conversion->getStage1Data();
|
||||
$startingWeight = $stage1Data['starting_weight'];
|
||||
|
||||
// Calculate individual percentages
|
||||
$freshPressPercentage = $startingWeight > 0 ? round(($validated['fresh_press_120u'] / $startingWeight) * 100, 2) : 0;
|
||||
$coldCurePercentage = $startingWeight > 0 ? round(($validated['cold_cure_90u'] / $startingWeight) * 100, 2) : 0;
|
||||
$rosinPercentage = $startingWeight > 0 ? round(($validated['rosin_45u'] / $startingWeight) * 100, 2) : 0;
|
||||
$greenBlonde160Percentage = $startingWeight > 0 ? round(($validated['green_blonde_160u'] / $startingWeight) * 100, 2) : 0;
|
||||
$greenBlonde25Percentage = $startingWeight > 0 ? round(($validated['green_blonde_25u'] / $startingWeight) * 100, 2) : 0;
|
||||
|
||||
// Calculate total yield
|
||||
$totalYield = $validated['fresh_press_120u']
|
||||
+ $validated['cold_cure_90u']
|
||||
+ $validated['rosin_45u']
|
||||
+ $validated['green_blonde_160u']
|
||||
+ $validated['green_blonde_25u'];
|
||||
|
||||
// Update conversion with Stage 2 data
|
||||
$metadata = $conversion->metadata;
|
||||
$metadata['stage_2'] = [
|
||||
'yields' => [
|
||||
'fresh_press_120u' => [
|
||||
'weight' => (float) $validated['fresh_press_120u'],
|
||||
'percentage' => $freshPressPercentage,
|
||||
],
|
||||
'cold_cure_90u' => [
|
||||
'weight' => (float) $validated['cold_cure_90u'],
|
||||
'percentage' => $coldCurePercentage,
|
||||
],
|
||||
'rosin_45u' => [
|
||||
'weight' => (float) $validated['rosin_45u'],
|
||||
'percentage' => $rosinPercentage,
|
||||
],
|
||||
'green_blonde_160u' => [
|
||||
'weight' => (float) $validated['green_blonde_160u'],
|
||||
'percentage' => $greenBlonde160Percentage,
|
||||
],
|
||||
'green_blonde_25u' => [
|
||||
'weight' => (float) $validated['green_blonde_25u'],
|
||||
'percentage' => $greenBlonde25Percentage,
|
||||
],
|
||||
],
|
||||
'total_yield' => $totalYield,
|
||||
];
|
||||
|
||||
$conversion->metadata = $metadata;
|
||||
$conversion->actual_output_quantity = $totalYield;
|
||||
$conversion->actual_output_unit = 'g';
|
||||
$conversion->save();
|
||||
|
||||
return redirect()->route('seller.business.wash-reports.show', [
|
||||
'business' => $business->slug,
|
||||
'conversion' => $conversion->id,
|
||||
])->with('success', 'Wash report completed successfully! Total yield: '.$totalYield.'g ('.$conversion->getYieldPercentage().'%)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a single wash report
|
||||
*/
|
||||
public function show(Business $business, Conversion $conversion)
|
||||
{
|
||||
// Verify conversion belongs to business
|
||||
if ($conversion->business_id !== $business->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$conversion->load(['operator', 'inputBatches', 'batchCreated']);
|
||||
|
||||
return view('seller.wash-reports.show', compact('business', 'conversion'));
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Traits\FileStorageHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class StorageTestController extends Controller
|
||||
{
|
||||
use FileStorageHelper;
|
||||
|
||||
/**
|
||||
* Test storage configuration
|
||||
*/
|
||||
public function test(Request $request)
|
||||
{
|
||||
$results = [];
|
||||
$results['storage_info'] = $this->getStorageInfo();
|
||||
|
||||
// Test file upload if provided
|
||||
if ($request->hasFile('test_file')) {
|
||||
try {
|
||||
$file = $request->file('test_file');
|
||||
|
||||
// Store test file
|
||||
$path = $this->storeFile($file, 'tests');
|
||||
$results['upload'] = [
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
'url' => $this->getFileUrl($path),
|
||||
];
|
||||
|
||||
// Verify file exists
|
||||
$disk = Storage::disk($this->getStorageDisk());
|
||||
$results['verification'] = [
|
||||
'exists' => $disk->exists($path),
|
||||
'size' => $disk->size($path),
|
||||
];
|
||||
|
||||
// Delete test file
|
||||
$deleted = $this->deleteFile($path);
|
||||
$results['cleanup'] = [
|
||||
'deleted' => $deleted,
|
||||
'still_exists' => $disk->exists($path),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results['error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($results, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show test upload form
|
||||
*/
|
||||
public function form()
|
||||
{
|
||||
return view('storage-test');
|
||||
}
|
||||
}
|
||||
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, 'category_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ComponentCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(ComponentCategory::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
110
app/Models/Conversion.php
Normal file
110
app/Models/Conversion.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?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\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* Conversion Model
|
||||
*
|
||||
* Represents a manufacturing conversion process where input materials/batches
|
||||
* are transformed into output products/batches.
|
||||
*
|
||||
* Examples:
|
||||
* - hash_wash: Converting raw material into washed hash
|
||||
* - trim_to_extract: Converting trim into concentrate
|
||||
* - flower_to_preroll: Converting flower into pre-rolls
|
||||
*/
|
||||
class Conversion extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'business_id',
|
||||
'conversion_type',
|
||||
'internal_name',
|
||||
'operator_id',
|
||||
'operator_user_id',
|
||||
'conversion_date',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'notes',
|
||||
'status',
|
||||
'input_weight',
|
||||
'output_weight',
|
||||
'yield_percentage',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'conversion_date' => 'datetime',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
'input_weight' => 'decimal:2',
|
||||
'output_weight' => 'decimal:2',
|
||||
'yield_percentage' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the business that owns the conversion
|
||||
*/
|
||||
public function business(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Business::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the operator (user) who performed the conversion
|
||||
*/
|
||||
public function operator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'operator_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input batches used in this conversion
|
||||
*/
|
||||
public function inputBatches(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Batch::class, 'conversion_inputs')
|
||||
->withPivot(['quantity', 'unit', 'notes'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the batch created from this conversion
|
||||
*/
|
||||
public function batchCreated(): HasOne
|
||||
{
|
||||
return $this->hasOne(Batch::class, 'conversion_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by conversion type
|
||||
*/
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('conversion_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by business
|
||||
*/
|
||||
public function scopeForBusiness($query, int $businessId)
|
||||
{
|
||||
return $query->where('business_id', $businessId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Stage 1 data from metadata
|
||||
*/
|
||||
public function getStage1Data(): ?array
|
||||
{
|
||||
return $this->metadata['stage_1'] ?? null;
|
||||
}
|
||||
}
|
||||
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, 'category_id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ class ProductImage extends Model
|
||||
'path',
|
||||
'type',
|
||||
'order',
|
||||
'sort_order',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function product(): BelongsTo
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -1,56 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const content = fs.readFileSync('resources/views/seller/products/edit11.blade.php', 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let depth = 0;
|
||||
const stack = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const lineNum = i + 1;
|
||||
|
||||
// Skip lines that are Alpine.js @error handlers
|
||||
if (line.includes('@error') && line.includes('$event')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for @if (but not in @endif, @error, @enderror)
|
||||
if (/@if\s*\(/.test(line) && !/@endif/.test(line)) {
|
||||
depth++;
|
||||
stack.push({ line: lineNum, type: 'if', content: line.trim().substring(0, 80) });
|
||||
console.log(`${lineNum}: [depth +${depth}] @if`);
|
||||
}
|
||||
|
||||
// Check for @elseif
|
||||
if (/@elseif\s*\(/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @elseif`);
|
||||
}
|
||||
|
||||
// Check for @else (but not @elseif, @endforelse, @enderror)
|
||||
if (/@else\b/.test(line) && !/@elseif/.test(line) && !/@endforelse/.test(line) && !/@enderror/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @else`);
|
||||
}
|
||||
|
||||
// Check for @endif
|
||||
if (/@endif\b/.test(line)) {
|
||||
console.log(`${lineNum}: [depth -${depth}] @endif`);
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
stack.pop();
|
||||
} else {
|
||||
console.log(`ERROR: Extra @endif at line ${lineNum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFinal depth: ${depth}`);
|
||||
if (depth > 0) {
|
||||
console.log(`\nUNBALANCED: Missing ${depth} @endif statement(s)`);
|
||||
console.log('\nUnclosed @if statements:');
|
||||
stack.forEach(item => {
|
||||
console.log(` Line ${item.line}: ${item.content}`);
|
||||
});
|
||||
} else {
|
||||
console.log('\nAll @if/@endif pairs are balanced!');
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
$file = 'C:\Users\Boss Man\Documents\GitHub\hub\resources\views\seller\products\edit11.blade.php';
|
||||
$lines = file($file);
|
||||
|
||||
$stack = [];
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
$lineNum++; // 1-indexed
|
||||
|
||||
// Check for @if (but not @endif, @elseif, etc.)
|
||||
if (preg_match('/^\s*@if\(/', $line)) {
|
||||
$stack[] = ['type' => 'if', 'line' => $lineNum];
|
||||
echo "Line $lineNum: OPEN @if (stack depth: ".count($stack).")\n";
|
||||
}
|
||||
// Check for @elseif
|
||||
elseif (preg_match('/^\s*@elseif\(/', $line)) {
|
||||
echo "Line $lineNum: @elseif\n";
|
||||
}
|
||||
// Check for @else
|
||||
elseif (preg_match('/^\s*@else\s*$/', $line)) {
|
||||
echo "Line $lineNum: @else\n";
|
||||
}
|
||||
// Check for @endif
|
||||
elseif (preg_match('/^\s*@endif\s*$/', $line)) {
|
||||
if (empty($stack)) {
|
||||
echo "ERROR Line $lineNum: @endif without matching @if!\n";
|
||||
} else {
|
||||
$opened = array_pop($stack);
|
||||
echo "Line $lineNum: CLOSE @endif (opened at line {$opened['line']}, stack depth: ".count($stack).")\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($stack)) {
|
||||
echo "\nERROR: Unclosed @if directives:\n";
|
||||
foreach ($stack as $item) {
|
||||
echo " Line {$item['line']}: @if never closed\n";
|
||||
}
|
||||
} else {
|
||||
echo "\nAll @if/@endif directives are balanced!\n";
|
||||
}
|
||||
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">{{ $user->name }}</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>
|
||||
@@ -44,13 +44,18 @@
|
||||
<!-- Brand Context Switcher -->
|
||||
<x-brand-switcher />
|
||||
|
||||
<!-- View Switcher (Sales/Manufacturing/Compliance) -->
|
||||
<x-view-switcher />
|
||||
|
||||
<div class="mb-3 space-y-0.5 px-2.5" x-data="{
|
||||
menuDashboard: $persist(true).as('sidebar-menu-dashboard'),
|
||||
menuOrders: $persist(false).as('sidebar-menu-orders'),
|
||||
menuInvoices: $persist(false).as('sidebar-menu-invoices'),
|
||||
menuBrands: $persist(true).as('sidebar-menu-brands'),
|
||||
menuInventory: $persist(true).as('sidebar-menu-inventory'),
|
||||
menuCustomers: $persist(false).as('sidebar-menu-customers'),
|
||||
menuFleet: $persist(true).as('sidebar-menu-fleet'),
|
||||
menuManufacturing: $persist(true).as('sidebar-menu-manufacturing'),
|
||||
menuBusiness: $persist(true).as('sidebar-menu-business')
|
||||
}">
|
||||
<p class="menu-label px-2.5 pt-3 pb-1.5 first:pt-0">Overview</p>
|
||||
@@ -162,6 +167,38 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="group collapse">
|
||||
<input
|
||||
aria-label="Sidemenu item trigger"
|
||||
type="checkbox"
|
||||
class="peer"
|
||||
name="sidebar-menu-parent-item"
|
||||
x-model="menuBrands" />
|
||||
<div class="collapse-title px-2.5 py-1.5">
|
||||
<span class="icon-[lucide--bookmark] size-4"></span>
|
||||
<span class="grow">Brands</span>
|
||||
<span class="icon-[lucide--chevron-right] arrow-icon size-3.5"></span>
|
||||
</div>
|
||||
@if($sidebarBusiness)
|
||||
<div class="collapse-content ms-6.5 !p-0">
|
||||
<div class="mt-0.5 space-y-0.5">
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.brands.index') ? 'active' : '' }}" href="{{ route('seller.business.brands.index', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">All Brands</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.brands.create') ? 'active' : '' }}" href="{{ route('seller.business.brands.create', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Create Brand</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="collapse-content ms-6.5 !p-0">
|
||||
<div class="mt-0.5 px-3 py-2">
|
||||
<p class="text-xs text-base-content/60">Complete your business profile to manage brands</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="group collapse">
|
||||
<input
|
||||
aria-label="Sidemenu item trigger"
|
||||
@@ -254,6 +291,41 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="group collapse">
|
||||
<input
|
||||
aria-label="Sidemenu item trigger"
|
||||
type="checkbox"
|
||||
class="peer"
|
||||
name="sidebar-menu-parent-item"
|
||||
x-model="menuManufacturing" />
|
||||
<div class="collapse-title px-2.5 py-1.5">
|
||||
<span class="icon-[lucide--factory] size-4"></span>
|
||||
<span class="grow">Manufacturing</span>
|
||||
<span class="icon-[lucide--chevron-right] arrow-icon size-3.5"></span>
|
||||
</div>
|
||||
@if($sidebarBusiness)
|
||||
<div class="collapse-content ms-6.5 !p-0">
|
||||
<div class="mt-0.5 space-y-0.5">
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.batches.index') ? 'active' : '' }}" href="{{ route('seller.business.batches.index', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">All Batches</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.batches.create') ? 'active' : '' }}" href="{{ route('seller.business.batches.create', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Create Batch</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.wash-reports.*') ? 'active' : '' }}" href="{{ route('seller.business.wash-reports.index', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Wash Reports</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="collapse-content ms-6.5 !p-0">
|
||||
<div class="mt-0.5 px-3 py-2">
|
||||
<p class="text-xs text-base-content/60">Complete your business profile to manage manufacturing</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p class="menu-label px-2.5 pt-3 pb-1.5 first:pt-0">Business</p>
|
||||
@if(auth()->user()?->hasRole('super-admin'))
|
||||
<a class="menu-item {{ request()->routeIs('filament.admin.*') ? 'active' : '' }}" href="{{ route('filament.admin.pages.dashboard') }}">
|
||||
@@ -283,17 +355,17 @@
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.users') ? 'active' : '' }}" href="{{ route('seller.business.settings.users', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Users</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.orders') ? 'active' : '' }}" href="{{ route('seller.business.settings.orders', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Orders</span>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.sales-config') ? 'active' : '' }}" href="{{ route('seller.business.settings.sales-config', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Sales Config</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.brands') ? 'active' : '' }}" href="{{ route('seller.business.settings.brands', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Brands</span>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.brand-kit') ? 'active' : '' }}" href="{{ route('seller.business.settings.brand-kit', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Brand Kit</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.payments') ? 'active' : '' }}" href="{{ route('seller.business.settings.payments', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Payments</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.invoices') ? 'active' : '' }}" href="{{ route('seller.business.settings.invoices', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Invoices</span>
|
||||
<span class="grow">Invoice Settings</span>
|
||||
</a>
|
||||
<a class="menu-item {{ request()->routeIs('seller.business.settings.manage-licenses') ? 'active' : '' }}" href="{{ route('seller.business.settings.manage-licenses', $sidebarBusiness->slug) }}">
|
||||
<span class="grow">Manage Licenses</span>
|
||||
@@ -354,8 +426,11 @@
|
||||
<div class="mb-2">
|
||||
<hr class="border-base-300 my-2 border-dashed" />
|
||||
|
||||
<!-- Owner Account Dropdown (moved above version info) -->
|
||||
<x-seller-account-dropdown />
|
||||
|
||||
<!-- Version Info Section -->
|
||||
<div class="mx-2 mb-3 px-3 text-xs text-center text-base-content/50">
|
||||
<div class="mx-2 mt-3 mb-3 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 +439,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, {{ config('version.company.suffix') }}</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>
|
||||
388
resources/views/seller/batches/create.blade.php
Normal file
388
resources/views/seller/batches/create.blade.php
Normal file
@@ -0,0 +1,388 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 text-sm breadcrumbs mb-2">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.batches.index', $business->slug) }}">Batches</a></li>
|
||||
<li>Create Batch</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<span class="icon-[lucide--package-2] size-8"></span>
|
||||
Create Batch
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Create a tested batch of finite material - See <a href="{{ asset('docs/BATCH_AND_LAB_SYSTEM.md') }}" class="link" target="_blank">architecture docs</a></p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.batches.store', $business->slug) }}" class="space-y-6 max-w-5xl" x-data="{
|
||||
batchType: '{{ old('batch_type', 'component') }}',
|
||||
sourceComponents: {{ old('source_components', '[]') }},
|
||||
addSourceComponent() {
|
||||
this.sourceComponents.push({ batch_id: '', quantity_used: '', unit: 'lbs' });
|
||||
},
|
||||
removeSourceComponent(index) {
|
||||
this.sourceComponents.splice(index, 1);
|
||||
}
|
||||
}">
|
||||
@csrf
|
||||
|
||||
<!-- Batch Type Selection -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--split] size-5"></span>
|
||||
Batch Type
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Select the type of batch you're creating</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Component Batch Option -->
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="batch_type" value="component" class="radio radio-primary" x-model="batchType" {{ old('batch_type', 'component') === 'component' ? 'checked' : '' }} />
|
||||
<div class="card bg-base-200 border-2 transition-all" :class="batchType === 'component' ? 'border-primary' : 'border-transparent'">
|
||||
<div class="card-body">
|
||||
<h4 class="font-semibold flex items-center gap-2">
|
||||
<span class="icon-[lucide--package] size-5"></span>
|
||||
Component Batch
|
||||
</h4>
|
||||
<p class="text-sm text-base-content/60">Tested input material (flower, rosin, etc.) that SKUs pull from</p>
|
||||
<div class="text-xs text-base-content/50 mt-2">
|
||||
Example: 500 lbs of tested flower batch that multiple jar SKUs use
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- Homogenized Batch Option -->
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="batch_type" value="homogenized" class="radio radio-primary" x-model="batchType" {{ old('batch_type') === 'homogenized' ? 'checked' : '' }} />
|
||||
<div class="card bg-base-200 border-2 transition-all" :class="batchType === 'homogenized' ? 'border-primary' : 'border-transparent'">
|
||||
<div class="card-body">
|
||||
<h4 class="font-semibold flex items-center gap-2">
|
||||
<span class="icon-[lucide--combine] size-5"></span>
|
||||
Homogenized Batch
|
||||
</h4>
|
||||
<p class="text-sm text-base-content/60">Mixed components requiring new testing with multiple COAs</p>
|
||||
<div class="text-xs text-base-content/50 mt-2">
|
||||
Example: Enhanced preroll mixing flower + rosin, shows both source COAs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@error('batch_type')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Batch Information -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--file-text] size-5"></span>
|
||||
Batch Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Component (only for component batches) -->
|
||||
<div class="form-control md:col-span-2" x-show="batchType === 'component'">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Component <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<div x-data="{
|
||||
init() {
|
||||
if (this.batchType === 'component') {
|
||||
new Choices(this.$refs.componentSelect, {
|
||||
searchEnabled: true,
|
||||
searchPlaceholderValue: 'Type to search components...',
|
||||
shouldSort: false,
|
||||
placeholder: true,
|
||||
placeholderValue: 'Select a component...',
|
||||
noResultsText: 'No components found',
|
||||
noChoicesText: 'No components available',
|
||||
allowHTML: false,
|
||||
itemSelectText: 'Press to select',
|
||||
})
|
||||
}
|
||||
}
|
||||
}">
|
||||
<select x-ref="componentSelect" name="component_id" class="select select-bordered w-full @error('component_id') select-error @enderror" :required="batchType === 'component'">
|
||||
<option value="">Select a component...</option>
|
||||
@foreach($components as $component)
|
||||
<option value="{{ $component->id }}" {{ old('component_id') == $component->id ? 'selected' : '' }}>
|
||||
{{ $component->name }} ({{ $component->sku }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@error('component_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">The component this batch represents (e.g., Flower, Rosin)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Batch Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Batch Number <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="batch_number" class="input input-bordered @error('batch_number') input-error @enderror" value="{{ old('batch_number') }}" placeholder="e.g., CB-240315-001" required />
|
||||
@error('batch_number')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">Unique batch identifier</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Internal Code -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Internal Code</span>
|
||||
</label>
|
||||
<input type="text" name="internal_code" class="input input-bordered @error('internal_code') input-error @enderror" value="{{ old('internal_code') }}" placeholder="Internal tracking code" />
|
||||
@error('internal_code')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Production Date -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Production Date <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="date" name="production_date" class="input input-bordered @error('production_date') input-error @enderror" value="{{ old('production_date', now()->format('Y-m-d')) }}" required />
|
||||
@error('production_date')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Expiration Date -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Expiration Date</span>
|
||||
</label>
|
||||
<input type="date" name="expiration_date" class="input input-bordered @error('expiration_date') input-error @enderror" value="{{ old('expiration_date') }}" />
|
||||
@error('expiration_date')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory Tracking -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--package-check] size-5"></span>
|
||||
Inventory Tracking
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Track finite quantity - depletes as SKUs consume from this batch</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Total Quantity -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Total Quantity <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="quantity_total" class="input input-bordered @error('quantity_total') input-error @enderror" value="{{ old('quantity_total') }}" placeholder="500" required />
|
||||
@error('quantity_total')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Remaining Quantity (defaults to total) -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Remaining Quantity <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="quantity_remaining" class="input input-bordered @error('quantity_remaining') input-error @enderror" value="{{ old('quantity_remaining', old('quantity_total')) }}" placeholder="500" required />
|
||||
@error('quantity_remaining')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">Usually same as total initially</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Unit -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Unit <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select name="quantity_unit" class="select select-bordered @error('quantity_unit') select-error @enderror" required>
|
||||
<option value="">-- Select Unit --</option>
|
||||
<option value="g" {{ old('quantity_unit', 'g') === 'g' ? 'selected' : '' }}>g (grams)</option>
|
||||
<option value="lbs" {{ old('quantity_unit') === 'lbs' ? 'selected' : '' }}>lbs (pounds)</option>
|
||||
<option value="kg" {{ old('quantity_unit') === 'kg' ? 'selected' : '' }}>kg (kilograms)</option>
|
||||
<option value="oz" {{ old('quantity_unit') === 'oz' ? 'selected' : '' }}>oz (ounces)</option>
|
||||
<option value="units" {{ old('quantity_unit') === 'units' ? 'selected' : '' }}>units</option>
|
||||
</select>
|
||||
@error('quantity_unit')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div class="text-sm">
|
||||
<strong>How it works:</strong> As SKUs are linked to this batch, quantity_remaining depletes. When it reaches zero, you'll need to create a new batch with new testing.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source Components (only for homogenized batches) -->
|
||||
<div class="card bg-base-100 shadow-sm" x-show="batchType === 'homogenized'">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--git-merge] size-5"></span>
|
||||
Source Components
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Which component batches were mixed to create this homogenized batch?</p>
|
||||
|
||||
<template x-for="(source, index) in sourceComponents" :key="index">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 mb-4 items-end">
|
||||
<!-- Source Batch -->
|
||||
<div class="form-control md:col-span-6">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Source Batch <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select :name="'source_components[' + index + '][batch_id]'" class="select select-bordered w-full" required>
|
||||
<option value="">Select source batch...</option>
|
||||
@foreach($componentBatches as $batch)
|
||||
<option value="{{ $batch->id }}">
|
||||
{{ $batch->batch_number }} - {{ $batch->component->name ?? 'Unknown' }} ({{ $batch->quantity_remaining }} {{ $batch->quantity_unit }} remaining)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Used -->
|
||||
<div class="form-control md:col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Quantity Used <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" :name="'source_components[' + index + '][quantity_used]'" class="input input-bordered" placeholder="10" required />
|
||||
</div>
|
||||
|
||||
<!-- Unit -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Unit <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select :name="'source_components[' + index + '][unit]'" class="select select-bordered" required>
|
||||
<option value="lbs">lbs</option>
|
||||
<option value="g">g</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="oz">oz</option>
|
||||
<option value="units">units</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Remove Button -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<button type="button" @click="removeSourceComponent(index)" class="btn btn-error btn-square">
|
||||
<span class="icon-[lucide--trash-2] size-4"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="button" @click="addSourceComponent()" class="btn btn-outline btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Source Component
|
||||
</button>
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<span class="icon-[lucide--alert-triangle] size-5"></span>
|
||||
<div class="text-sm">
|
||||
<strong>Important:</strong> QR codes for homogenized batches will display multiple COAs - the primary COA for this batch PLUS all source component COAs for full traceability.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Flags -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--toggle-left] size-5"></span>
|
||||
Status
|
||||
</h3>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="is_active" class="checkbox checkbox-primary" {{ old('is_active', true) ? 'checked' : '' }} />
|
||||
<div>
|
||||
<span class="label-text font-medium">Active</span>
|
||||
<p class="text-xs text-base-content/60">Is this batch available for use?</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields - batches are tested by default (you have COA) and not quarantined -->
|
||||
<input type="hidden" name="is_tested" value="1" />
|
||||
<input type="hidden" name="is_quarantined" value="0" />
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div class="text-sm">
|
||||
<strong>Note:</strong> Batches are automatically marked as "tested" since you're creating them with a passed COA. They're not quarantined by default.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Notes -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--sticky-note] size-5"></span>
|
||||
Additional Notes
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Optional production notes, special handling, etc.</p>
|
||||
<div class="form-control">
|
||||
<textarea name="notes" rows="4" class="textarea textarea-bordered @error('notes') textarea-error @enderror" placeholder="e.g., Special growing conditions, handling instructions...">{{ old('notes') }}</textarea>
|
||||
@error('notes')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<a href="{{ route('seller.business.batches.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-[lucide--check] size-4"></span>
|
||||
Create Batch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-info max-w-5xl mt-6">
|
||||
<span class="icon-[lucide--lightbulb] size-5"></span>
|
||||
<div>
|
||||
<h4 class="font-semibold">Next Steps After Creating Batch</h4>
|
||||
<ol class="text-sm list-decimal list-inside mt-2 space-y-1">
|
||||
<li>Upload or link the primary COA (Lab/Test Result)</li>
|
||||
<li>Link this batch to specific SKU variants (NOT parent products) via the product_batches pivot</li>
|
||||
<li>Generate QR code for display on packaging</li>
|
||||
<li>Track quantity_remaining as SKUs consume from this batch</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
543
resources/views/seller/batches/edit.blade.php
Normal file
543
resources/views/seller/batches/edit.blade.php
Normal file
@@ -0,0 +1,543 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 text-sm breadcrumbs mb-2">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.batches.index', $business->slug) }}">Batches</a></li>
|
||||
<li>Edit Batch</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<span class="icon-[lucide--package-2] size-8"></span>
|
||||
Edit Batch
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Update batch information and test results</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.batches.update', [$business->slug, $batch->id]) }}" enctype="multipart/form-data" class="space-y-6 max-w-5xl">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Batch Information -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--package] size-5"></span>
|
||||
Batch Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">SKU / Product <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<div x-data="{
|
||||
init() {
|
||||
new Choices(this.$refs.productSelect, {
|
||||
searchEnabled: true,
|
||||
searchPlaceholderValue: 'Type to search products...',
|
||||
shouldSort: false,
|
||||
placeholder: true,
|
||||
placeholderValue: 'Select a product...',
|
||||
noResultsText: 'No products found',
|
||||
noChoicesText: 'No products available',
|
||||
allowHTML: false,
|
||||
itemSelectText: 'Press to select',
|
||||
})
|
||||
}
|
||||
}">
|
||||
<select x-ref="productSelect" name="product_id" class="select select-bordered w-full @error('product_id') select-error @enderror" required>
|
||||
<option value="">Select a product...</option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}" {{ old('product_id', $batch->product_id) == $product->id ? 'selected' : '' }}>
|
||||
{{ $product->name }} ({{ $product->brand->name ?? 'No Brand' }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@error('product_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Production Batch Number</span>
|
||||
</label>
|
||||
<input type="text" name="batch_number" class="input input-bordered @error('batch_number') input-error @enderror" value="{{ old('batch_number', $batch->batch_number) }}" placeholder="Auto-generated if left blank" />
|
||||
@error('batch_number')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">Leave blank to auto-generate</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Batch Date</span>
|
||||
</label>
|
||||
<input type="date" name="production_date" class="input input-bordered @error('production_date') input-error @enderror" value="{{ old('production_date', $batch->production_date?->format('Y-m-d')) }}" />
|
||||
@error('production_date')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Lot Number</span>
|
||||
</label>
|
||||
<input type="text" name="lot_number" class="input input-bordered @error('lot_number') input-error @enderror" value="{{ old('lot_number', $batch->lot_number) }}" placeholder="e.g., LOT-2024-001" />
|
||||
@error('lot_number')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test ID</span>
|
||||
</label>
|
||||
<input type="text" name="test_id" class="input input-bordered @error('test_id') input-error @enderror" value="{{ old('test_id', $batch->test_id) }}" placeholder="e.g., LAB-2024-001" />
|
||||
@error('test_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Date</span>
|
||||
</label>
|
||||
<input type="date" name="test_date" class="input input-bordered @error('test_date') input-error @enderror" value="{{ old('test_date', $batch->test_date?->format('Y-m-d')) }}" />
|
||||
@error('test_date')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Lab Name</span>
|
||||
</label>
|
||||
<input type="text" name="lab_name" class="input input-bordered @error('lab_name') input-error @enderror" value="{{ old('lab_name', $batch->lab_name) }}" placeholder="e.g., SC Labs" />
|
||||
@error('lab_name')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Results (Inline Cannabinoids - Leaflink Style) -->
|
||||
<div class="card bg-base-100 shadow-sm" x-data="{
|
||||
cannabinoidUnit: '{{ old('cannabinoid_unit', $batch->cannabinoid_unit ?? '%') }}',
|
||||
get displayUnit() {
|
||||
const units = {
|
||||
'%': '%',
|
||||
'MG/ML': 'mg/ml',
|
||||
'MG/G': 'mg/g',
|
||||
'MG/UNIT': 'mg/unit'
|
||||
};
|
||||
return units[this.cannabinoidUnit] || '%';
|
||||
}
|
||||
}">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--flask-conical] size-5"></span>
|
||||
Test Results
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Enter cannabinoid values from the COA</p>
|
||||
|
||||
<!-- Cannabinoid Unit Selector -->
|
||||
<div class="mb-6">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Cannabinoid Unit <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select name="cannabinoid_unit" x-model="cannabinoidUnit" class="select select-bordered w-full max-w-xs @error('cannabinoid_unit') select-error @enderror" required>
|
||||
<option value="%" {{ old('cannabinoid_unit', $batch->cannabinoid_unit ?? '%') == '%' ? 'selected' : '' }}>% (Percentage)</option>
|
||||
<option value="MG/ML" {{ old('cannabinoid_unit', $batch->cannabinoid_unit) == 'MG/ML' ? 'selected' : '' }}>MG/ML (Milligrams per Milliliter)</option>
|
||||
<option value="MG/G" {{ old('cannabinoid_unit', $batch->cannabinoid_unit) == 'MG/G' ? 'selected' : '' }}>MG/G (Milligrams per Gram)</option>
|
||||
<option value="MG/UNIT" {{ old('cannabinoid_unit', $batch->cannabinoid_unit) == 'MG/UNIT' ? 'selected' : '' }}>MG/UNIT (Milligrams per Unit)</option>
|
||||
</select>
|
||||
@error('cannabinoid_unit')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">Select the unit of measurement for cannabinoid values</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="divider my-4">Cannabinoid Values</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">THC <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="thc_percentage" class="input input-bordered @error('thc_percentage') input-error @enderror" value="{{ old('thc_percentage', $batch->thc_percentage) }}" placeholder="0.00" />
|
||||
@error('thc_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">THCa <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="thca_percentage" class="input input-bordered @error('thca_percentage') input-error @enderror" value="{{ old('thca_percentage', $batch->thca_percentage) }}" placeholder="0.00" />
|
||||
@error('thca_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">CBD <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="cbd_percentage" class="input input-bordered @error('cbd_percentage') input-error @enderror" value="{{ old('cbd_percentage', $batch->cbd_percentage) }}" placeholder="0.00" />
|
||||
@error('cbd_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">CBDa <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="cbda_percentage" class="input input-bordered @error('cbda_percentage') input-error @enderror" value="{{ old('cbda_percentage', $batch->cbda_percentage) }}" placeholder="0.00" />
|
||||
@error('cbda_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">CBG <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="cbg_percentage" class="input input-bordered @error('cbg_percentage') input-error @enderror" value="{{ old('cbg_percentage', $batch->cbg_percentage) }}" placeholder="0.00" />
|
||||
@error('cbg_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">CBN <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="cbn_percentage" class="input input-bordered @error('cbn_percentage') input-error @enderror" value="{{ old('cbn_percentage', $batch->cbn_percentage) }}" placeholder="0.00" />
|
||||
@error('cbn_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Delta 9 <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="delta_9_percentage" class="input input-bordered @error('delta_9_percentage') input-error @enderror" value="{{ old('delta_9_percentage', $batch->delta_9_percentage) }}" placeholder="0.00" />
|
||||
@error('delta_9_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Total Terps <span x-text="displayUnit"></span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="total_terps_percentage" class="input input-bordered @error('total_terps_percentage') input-error @enderror" value="{{ old('total_terps_percentage', $batch->total_terps_percentage) }}" placeholder="0.00" />
|
||||
@error('total_terps_percentage')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="alert alert-info shadow-sm">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div>
|
||||
<h4 class="font-semibold">Auto-Calculated Values</h4>
|
||||
<p class="text-sm">Total THC, Total CBD, and Total Cannabinoids will be calculated automatically based on the values you enter above.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current COA Files -->
|
||||
@if($batch->coaFiles->count() > 0)
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--file-check] size-5"></span>
|
||||
Current COA Files
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Existing certificates of analysis</p>
|
||||
<div class="space-y-2">
|
||||
@foreach($batch->coaFiles as $coaFile)
|
||||
<div class="flex items-center justify-between p-3 bg-base-200 rounded-box">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="icon-[lucide--file-text] size-5 text-primary"></span>
|
||||
<div>
|
||||
<div class="font-medium">{{ $coaFile->file_name }}</div>
|
||||
<div class="text-xs text-base-content/60">
|
||||
{{ $coaFile->getFormattedSize() }} • Uploaded {{ $coaFile->created_at->diffForHumans() }}
|
||||
@if($coaFile->is_primary)
|
||||
<span class="badge badge-primary badge-xs ml-2">Primary</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ $coaFile->getUrl() }}" target="_blank" class="btn btn-ghost btn-sm">
|
||||
<span class="icon-[lucide--external-link] size-4"></span>
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Add New COA Files -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--file-plus] size-5"></span>
|
||||
Add New COA Files
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Upload additional certificates of analysis - PDF, JPG, or PNG format (max 10MB each)</p>
|
||||
<div class="form-control">
|
||||
<input type="file" name="coa_files[]" multiple accept=".pdf,.jpg,.jpeg,.png" class="file-input file-input-bordered w-full @error('coa_files.*') file-input-error @enderror" />
|
||||
@error('coa_files.*')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
<span class="icon-[lucide--info] size-3.5 inline"></span>
|
||||
You can upload multiple files at once
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Notes -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--sticky-note] size-5"></span>
|
||||
Additional Notes
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Optional - Add any additional information</p>
|
||||
<div class="form-control">
|
||||
<textarea name="notes" rows="4" class="textarea textarea-bordered @error('notes') textarea-error @enderror" placeholder="e.g., Retest results, special conditions, observations...">{{ old('notes', $batch->notes) }}</textarea>
|
||||
@error('notes')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<a href="{{ route('seller.business.batches.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-[lucide--check] size-4"></span>
|
||||
Update Batch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- QR Code Management (AJAX-powered, no page refresh) -->
|
||||
<div class="card bg-base-100 shadow-sm" x-data="{
|
||||
hasQrCode: {{ $batch->qr_code_path ? 'true' : 'false' }},
|
||||
qrCodeUrl: '{{ $batch->qr_code_path ? Storage::url($batch->qr_code_path) : '' }}',
|
||||
downloadUrl: '{{ $batch->qr_code_path ? route('seller.business.batches.qr-code.download', [$business->slug, $batch->id]) : '' }}',
|
||||
loading: false,
|
||||
message: '',
|
||||
messageType: '',
|
||||
|
||||
async generateQrCode() {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route('seller.business.batches.qr-code.generate', [$business->slug, $batch->id]) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.hasQrCode = true;
|
||||
this.qrCodeUrl = data.qr_code_url;
|
||||
this.downloadUrl = data.download_url;
|
||||
this.message = data.message;
|
||||
this.messageType = 'success';
|
||||
} else {
|
||||
this.message = data.message;
|
||||
this.messageType = 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
this.message = 'Failed to generate QR code. Please try again.';
|
||||
this.messageType = 'error';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async regenerateQrCode() {
|
||||
if (!confirm('Are you sure you want to regenerate the QR code?')) return;
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route('seller.business.batches.qr-code.regenerate', [$business->slug, $batch->id]) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.qrCodeUrl = data.qr_code_url + '?t=' + Date.now(); // Cache bust
|
||||
this.downloadUrl = data.download_url;
|
||||
this.message = data.message;
|
||||
this.messageType = 'success';
|
||||
} else {
|
||||
this.message = data.message;
|
||||
this.messageType = 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
this.message = 'Failed to regenerate QR code. Please try again.';
|
||||
this.messageType = 'error';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteQrCode() {
|
||||
if (!confirm('Are you sure you want to delete the QR code?')) return;
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
this.message = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('{{ route('seller.business.batches.qr-code.delete', [$business->slug, $batch->id]) }}', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.hasQrCode = false;
|
||||
this.qrCodeUrl = '';
|
||||
this.downloadUrl = '';
|
||||
this.message = data.message;
|
||||
this.messageType = 'success';
|
||||
} else {
|
||||
this.message = data.message;
|
||||
this.messageType = 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
this.message = 'Failed to delete QR code. Please try again.';
|
||||
this.messageType = 'error';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--qr-code] size-5"></span>
|
||||
QR Code
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Generate a QR code for easy access to this batch's COA</p>
|
||||
|
||||
<!-- Message Alert -->
|
||||
<div x-show="message" x-cloak class="mb-4">
|
||||
<div :class="messageType === 'success' ? 'alert alert-success' : 'alert alert-error'" class="shadow-sm">
|
||||
<span class="icon-[lucide--check-circle] size-5" x-show="messageType === 'success'"></span>
|
||||
<span class="icon-[lucide--alert-circle] size-5" x-show="messageType === 'error'"></span>
|
||||
<span x-text="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Display (when exists) -->
|
||||
<div x-show="hasQrCode" x-cloak>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img :src="qrCodeUrl"
|
||||
alt="Batch QR Code"
|
||||
class="w-48 h-48 border-2 border-base-300 rounded-box" />
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-3">
|
||||
<div class="alert alert-success shadow-sm">
|
||||
<span class="icon-[lucide--check-circle] size-5"></span>
|
||||
<span>QR code generated successfully</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a :href="downloadUrl"
|
||||
class="btn btn-primary btn-sm">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download QR Code
|
||||
</a>
|
||||
<button @click="regenerateQrCode()"
|
||||
:disabled="loading"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<span class="icon-[lucide--refresh-cw] size-4" :class="{ 'animate-spin': loading }"></span>
|
||||
<span x-text="loading ? 'Regenerating...' : 'Regenerate'"></span>
|
||||
</button>
|
||||
<button @click="deleteQrCode()"
|
||||
:disabled="loading"
|
||||
class="btn btn-error btn-sm">
|
||||
<span class="icon-[lucide--trash-2] size-4"></span>
|
||||
<span x-text="loading ? 'Deleting...' : 'Delete'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Button (when no QR code) -->
|
||||
<div x-show="!hasQrCode" x-cloak>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="alert alert-info shadow-sm">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<span>No QR code has been generated yet</span>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="generateQrCode()"
|
||||
:disabled="loading"
|
||||
class="btn btn-success">
|
||||
<span class="icon-[lucide--qr-code] size-4" :class="{ 'animate-spin': loading }"></span>
|
||||
<span x-text="loading ? 'Generating...' : 'Generate QR Code'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
234
resources/views/seller/batches/index.blade.php
Normal file
234
resources/views/seller/batches/index.blade.php
Normal file
@@ -0,0 +1,234 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@use('Illuminate\Support\Facades\Storage')
|
||||
<!-- Page Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<span class="icon-[lucide--package-2] size-8"></span>
|
||||
Batch Management
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Manage batches with integrated test results (COA data)</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('seller.business.batches.create', $business->slug) }}" class="btn btn-primary">
|
||||
<span class="icon-[lucide--plus] size-4.5"></span>
|
||||
Create Batch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div x-data="{ search: '{{ request('search') ?? '' }}' }">
|
||||
<div class="mt-6 card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex gap-3">
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
x-model="search"
|
||||
placeholder="Filter batches (or use Search All button)..."
|
||||
class="input input-bordered input-sm w-full pr-10"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
x-show="search"
|
||||
@click="search = ''"
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs btn-circle"
|
||||
>
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</button>
|
||||
</div>
|
||||
<form method="GET" class="flex gap-2">
|
||||
<input type="hidden" name="search" x-model="search" />
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-outline"
|
||||
:disabled="!search"
|
||||
>
|
||||
<span class="icon-[lucide--search] size-4"></span>
|
||||
Search All Batches
|
||||
</button>
|
||||
@if(request('search'))
|
||||
<a href="{{ route('seller.business.batches.index', $business->slug) }}" class="btn btn-sm btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
@if(request('search'))
|
||||
<div class="mt-3 alert alert-info">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
<span>Showing results for: <strong>{{ request('search') }}</strong></span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Batches Table -->
|
||||
@if($batches->count() > 0)
|
||||
<div class="mt-6 card bg-base-100 shadow" x-data="{ activeBatchesOpen: true }">
|
||||
<div class="card-body p-0">
|
||||
<!-- Active Batches Header -->
|
||||
<button @click="activeBatchesOpen = !activeBatchesOpen" class="flex items-center justify-between p-4 hover:bg-base-200 transition-colors">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span class="icon-[lucide--chevron-down] size-5 transition-transform" :class="activeBatchesOpen || 'rotate-[-90deg]'"></span>
|
||||
Active Batches
|
||||
<span class="badge badge-primary">{{ $batches->where('is_active', true)->count() }}</span>
|
||||
</h2>
|
||||
</button>
|
||||
|
||||
<div x-show="activeBatchesOpen" x-collapse>
|
||||
@if($batches->where('is_active', true)->count() > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr class="bg-base-200">
|
||||
<th class="font-mono text-xs">Batch #</th>
|
||||
<th class="text-xs">Unit</th>
|
||||
<th class="text-xs">THC</th>
|
||||
<th class="text-xs">THCa</th>
|
||||
<th class="text-xs">CBD</th>
|
||||
<th class="text-xs">CBDa</th>
|
||||
<th class="text-xs">CBG</th>
|
||||
<th class="text-xs">CBN</th>
|
||||
<th class="text-xs">Delta 8</th>
|
||||
<th class="text-xs">Total THC</th>
|
||||
<th class="text-xs">Total Cann.</th>
|
||||
<th>SKU</th>
|
||||
<th class="text-center">Doc</th>
|
||||
<th>Batch Date</th>
|
||||
<th class="w-12"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($batches->where('is_active', true) as $batch)
|
||||
<tr
|
||||
x-data="{
|
||||
batchNumber: '{{ $batch->batch_number }}',
|
||||
testId: '{{ $batch->test_id ?? '' }}',
|
||||
lotNumber: '{{ $batch->lot_number ?? '' }}',
|
||||
sku: '{{ $batch->product->name ?? '' }}'
|
||||
}"
|
||||
x-show="!search ||
|
||||
batchNumber.toLowerCase().includes(search.toLowerCase()) ||
|
||||
testId.toLowerCase().includes(search.toLowerCase()) ||
|
||||
lotNumber.toLowerCase().includes(search.toLowerCase()) ||
|
||||
sku.toLowerCase().includes(search.toLowerCase())"
|
||||
>
|
||||
<td><span class="font-mono text-xs">{{ $batch->batch_number }}</span></td>
|
||||
<td><span class="badge badge-sm badge-neutral">{{ $batch->cannabinoid_unit ?? '%' }}</span></td>
|
||||
<td class="text-xs">{{ $batch->thc_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->thca_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->cbd_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->cbda_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->cbg_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->cbn_percentage ?? '—' }}</td>
|
||||
<td class="text-xs">{{ $batch->delta_9_percentage ?? '—' }}</td>
|
||||
<td class="text-xs font-semibold">{{ $batch->total_thc ?? '—' }}</td>
|
||||
<td class="text-xs font-semibold">{{ $batch->total_cannabinoids ?? '—' }}</td>
|
||||
<td>
|
||||
<div class="text-sm">{{ $batch->product->name ?? 'N/A' }}</div>
|
||||
<div class="text-xs text-base-content/60">{{ $batch->product->brand->name ?? '' }}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if($batch->hasCoaFiles())
|
||||
<a href="{{ $batch->getCoaUrl() }}" target="_blank" class="btn btn-ghost btn-xs" title="View COA">
|
||||
<span class="icon-[lucide--file-text] size-4 text-primary"></span>
|
||||
</a>
|
||||
@else
|
||||
<span class="text-base-content/30">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-xs">{{ $batch->production_date ? $batch->production_date->format('M d, Y') : ($batch->test_date ? $batch->test_date->format('M d, Y') : '—') }}</td>
|
||||
<td>
|
||||
<div class="dropdown dropdown-end">
|
||||
<button tabindex="0" class="btn btn-ghost btn-xs">
|
||||
<span class="icon-[lucide--more-vertical] size-4"></span>
|
||||
</button>
|
||||
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[100] w-52 p-2 shadow-lg border border-base-300">
|
||||
@if($batch->hasCoaFiles())
|
||||
<li>
|
||||
<a href="{{ $batch->getCoaUrl() }}" target="_blank">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download COA
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li>
|
||||
<a href="{{ route('seller.business.batches.edit', [$business->slug, $batch->id]) }}">
|
||||
<span class="icon-[lucide--pencil] size-4"></span>
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ route('seller.business.batches.destroy', [$business->slug, $batch->id]) }}" method="POST" onsubmit="return confirm('Delete this batch?')" class="w-full">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-error w-full text-left">
|
||||
<span class="icon-[lucide--trash-2] size-4"></span>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="p-8 text-center text-base-content/60">
|
||||
No active batches found
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inactive Batches Table -->
|
||||
@if($batches->where('is_active', false)->count() > 0)
|
||||
<div class="mt-6 card bg-base-100 shadow" x-data="{ inactiveBatchesOpen: false }">
|
||||
<div class="card-body p-0">
|
||||
<!-- Inactive Batches Header -->
|
||||
<button @click="inactiveBatchesOpen = !inactiveBatchesOpen" class="flex items-center justify-between p-4 hover:bg-base-200 transition-colors">
|
||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span class="icon-[lucide--chevron-down] size-5 transition-transform" :class="inactiveBatchesOpen || 'rotate-[-90deg]'"></span>
|
||||
Inactive Batches
|
||||
<span class="badge">{{ $batches->where('is_active', false)->count() }}</span>
|
||||
</h2>
|
||||
</button>
|
||||
|
||||
<div x-show="inactiveBatchesOpen" x-collapse>
|
||||
<div class="p-8 text-center text-base-content/60">
|
||||
{{ $batches->where('is_active', false)->count() }} inactive batches
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-6">
|
||||
{{ $batches->links() }}
|
||||
</div>
|
||||
@else
|
||||
<!-- Empty State -->
|
||||
<div class="mt-6 card bg-base-100 shadow">
|
||||
<div class="card-body text-center py-12">
|
||||
<span class="icon-[lucide--package-2] size-16 text-base-content/20 mx-auto mb-4 block"></span>
|
||||
<h3 class="text-lg font-semibold mb-2">No batches found</h3>
|
||||
<p class="text-base-content/60 mb-4">Get started by creating your first batch with test results</p>
|
||||
<a href="{{ route('seller.business.batches.create', $business->slug) }}" class="btn btn-primary">
|
||||
<span class="icon-[lucide--plus] size-5"></span>
|
||||
Create Batch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
412
resources/views/seller/brands/create.blade.php
Normal file
412
resources/views/seller/brands/create.blade.php
Normal file
@@ -0,0 +1,412 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Add New Brand</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.brands.index', $business->slug) }}">Brands</a></li>
|
||||
<li class="opacity-80">Add New</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('seller.business.brands.store', $business->slug) }}" method="POST" enctype="multipart/form-data" class="mt-6">
|
||||
@csrf
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Brand Information</h2>
|
||||
|
||||
<!-- Brand Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Brand Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}"
|
||||
class="input input-bordered @error('name') input-error @enderror"
|
||||
placeholder="Enter brand name" required>
|
||||
@error('name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Tagline -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Tagline</span>
|
||||
</label>
|
||||
<input type="text" name="tagline" value="{{ old('tagline') }}"
|
||||
class="input input-bordered @error('tagline') input-error @enderror"
|
||||
placeholder="Enter brand tagline">
|
||||
@error('tagline')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Brand Website -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Brand Website</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<select name="website_protocol" class="select select-bordered join-item">
|
||||
<option value="https://" {{ old('website_protocol', 'https://') == 'https://' ? 'selected' : '' }}>https://</option>
|
||||
<option value="http://" {{ old('website_protocol') == 'http://' ? 'selected' : '' }}>http://</option>
|
||||
</select>
|
||||
<input type="text" name="website_domain" value="{{ old('website_domain') }}"
|
||||
class="input input-bordered join-item flex-1 @error('website_url') input-error @enderror"
|
||||
placeholder="www.example.com">
|
||||
</div>
|
||||
@error('website_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Short Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Short Description</span>
|
||||
<span class="label-text-alt" id="description-count">0/300</span>
|
||||
</label>
|
||||
<textarea name="description"
|
||||
class="textarea textarea-bordered h-24 @error('description') textarea-error @enderror"
|
||||
placeholder="Brief description of the brand"
|
||||
maxlength="300"
|
||||
oninput="updateCharCount('description', 'description-count', 300)">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Long Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Long Description</span>
|
||||
<span class="label-text-alt" id="long-description-count">0/700</span>
|
||||
</label>
|
||||
<textarea name="long_description"
|
||||
class="textarea textarea-bordered h-32 @error('long_description') textarea-error @enderror"
|
||||
placeholder="Detailed description of the brand"
|
||||
maxlength="700"
|
||||
oninput="updateCharCount('long_description', 'long-description-count', 700)">{{ old('long_description') }}</textarea>
|
||||
@error('long_description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3 class="font-semibold text-lg">Address Information</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Address -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Address</span>
|
||||
</label>
|
||||
<input type="text" name="address" value="{{ old('address') }}"
|
||||
class="input input-bordered @error('address') input-error @enderror"
|
||||
placeholder="Street address">
|
||||
@error('address')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Unit Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Unit Number</span>
|
||||
</label>
|
||||
<input type="text" name="unit_number" value="{{ old('unit_number') }}"
|
||||
class="input input-bordered @error('unit_number') input-error @enderror"
|
||||
placeholder="Unit, Suite, etc.">
|
||||
@error('unit_number')
|
||||
<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">Zip Code</span>
|
||||
</label>
|
||||
<input type="text" name="zip_code" value="{{ old('zip_code') }}"
|
||||
class="input input-bordered @error('zip_code') input-error @enderror"
|
||||
placeholder="12345">
|
||||
@error('zip_code')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- City -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">City</span>
|
||||
</label>
|
||||
<input type="text" name="city" value="{{ old('city') }}"
|
||||
class="input input-bordered @error('city') input-error @enderror"
|
||||
placeholder="City">
|
||||
@error('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">State</span>
|
||||
</label>
|
||||
<input type="text" name="state" value="{{ old('state') }}"
|
||||
class="input input-bordered @error('state') input-error @enderror"
|
||||
placeholder="AZ" maxlength="2">
|
||||
@error('state')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Phone -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Brand Phone</span>
|
||||
</label>
|
||||
<input type="tel" name="phone" value="{{ old('phone') }}"
|
||||
class="input input-bordered @error('phone') input-error @enderror"
|
||||
placeholder="(555) 123-4567">
|
||||
@error('phone')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3 class="font-semibold text-lg">Brand Images</h3>
|
||||
|
||||
<!-- Brand Image -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Brand Image (Logo)</span>
|
||||
</label>
|
||||
<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">Maximum file size: 2MB. Recommended: 500x500px</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">
|
||||
<img src="" alt="Logo preview" class="max-w-xs rounded-lg shadow-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Banner -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Brand Banner</span>
|
||||
</label>
|
||||
<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">Maximum file size: 4MB. Recommended: 1920x400px</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">
|
||||
<img src="" alt="Banner preview" class="max-w-full rounded-lg shadow-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3 class="font-semibold text-lg">Social Media</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Facebook -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Facebook</span>
|
||||
</label>
|
||||
<input type="url" name="facebook_url" value="{{ old('facebook_url') }}"
|
||||
class="input input-bordered @error('facebook_url') input-error @enderror"
|
||||
placeholder="https://facebook.com/yourbrand">
|
||||
@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">Instagram</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<span class="join-item flex items-center bg-base-200 px-3">@</span>
|
||||
<input type="text" name="instagram_handle" value="{{ old('instagram_handle') }}"
|
||||
class="input input-bordered join-item flex-1 @error('instagram_handle') input-error @enderror"
|
||||
placeholder="yourbrand">
|
||||
</div>
|
||||
@error('instagram_handle')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Twitter -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Twitter</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<span class="join-item flex items-center bg-base-200 px-3">@</span>
|
||||
<input type="text" name="twitter_handle" value="{{ old('twitter_handle') }}"
|
||||
class="input input-bordered join-item flex-1 @error('twitter_handle') input-error @enderror"
|
||||
placeholder="yourbrand">
|
||||
</div>
|
||||
@error('twitter_handle')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- YouTube -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">YouTube</span>
|
||||
</label>
|
||||
<input type="url" name="youtube_url" value="{{ old('youtube_url') }}"
|
||||
class="input input-bordered @error('youtube_url') input-error @enderror"
|
||||
placeholder="https://youtube.com/@yourbrand">
|
||||
@error('youtube_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<h3 class="font-semibold text-lg">Visibility</h3>
|
||||
|
||||
<!-- Public Menu Checkbox -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_public" value="1"
|
||||
{{ old('is_public') ? 'checked' : '' }}
|
||||
class="checkbox checkbox-primary">
|
||||
<div>
|
||||
<span class="label-text font-medium">Appears on Public Menu</span>
|
||||
<p class="text-sm text-base-content/60">Make this brand visible to buyers in the marketplace</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
{{ old('is_active', true) ? 'checked' : '' }}
|
||||
class="checkbox checkbox-primary">
|
||||
<div>
|
||||
<span class="label-text font-medium">Active</span>
|
||||
<p class="text-sm text-base-content/60">Brand is active and can be used for products</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Featured -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_featured" value="1"
|
||||
{{ old('is_featured') ? 'checked' : '' }}
|
||||
class="checkbox checkbox-primary">
|
||||
<div>
|
||||
<span class="label-text font-medium">Featured Brand</span>
|
||||
<p class="text-sm text-base-content/60">Highlight this brand on the marketplace</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-6 flex gap-3 justify-end">
|
||||
<a href="{{ route('seller.business.brands.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Create Brand
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Character count for textareas
|
||||
function updateCharCount(fieldName, counterId, maxLength) {
|
||||
const field = document.querySelector(`[name="${fieldName}"]`);
|
||||
const counter = document.getElementById(counterId);
|
||||
const currentLength = field.value.length;
|
||||
counter.textContent = `${currentLength}/${maxLength}`;
|
||||
}
|
||||
|
||||
// Initialize character counts on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateCharCount('description', 'description-count', 300);
|
||||
updateCharCount('long_description', 'long-description-count', 700);
|
||||
});
|
||||
|
||||
// Image preview functionality
|
||||
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]);
|
||||
} else {
|
||||
preview.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
520
resources/views/seller/brands/edit.blade.php
Normal file
520
resources/views/seller/brands/edit.blade.php
Normal file
@@ -0,0 +1,520 @@
|
||||
@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">Editing {{ $brand->name }}</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.brands.index', $business->slug) }}">Brands</a></li>
|
||||
<li class="opacity-80">{{ $brand->name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('seller.business.brands.update', [$business->slug, $brand]) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Brand Information Section -->
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Brand Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Brand Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Brand Name <span class="text-error">*</span></span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="The official name of your brand">
|
||||
<span class="iconify lucide--info size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $brand->name) }}"
|
||||
class="input input-bordered @error('name') input-error @enderror"
|
||||
placeholder="Enter brand name" required>
|
||||
@error('name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Tagline -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Tagline</span>
|
||||
<span class="label-text-alt text-sm" id="tagline-count">0/45</span>
|
||||
</label>
|
||||
<input type="text" name="tagline" value="{{ old('tagline', $brand->tagline) }}"
|
||||
class="input input-bordered @error('tagline') input-error @enderror"
|
||||
placeholder="A short, catchy phrase that represents your brand"
|
||||
maxlength="45"
|
||||
oninput="updateCharCount('tagline', 'tagline-count', 45)">
|
||||
@error('tagline')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<!-- Brand Website -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Brand Website</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Your brand's website URL (https:// added automatically)">
|
||||
<span class="iconify lucide--info size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
@php
|
||||
$websiteUrl = old('website_url', $brand->website_url);
|
||||
$websiteDomain = '';
|
||||
|
||||
if ($websiteUrl) {
|
||||
if (str_starts_with($websiteUrl, 'http://')) {
|
||||
$websiteDomain = substr($websiteUrl, 7);
|
||||
} elseif (str_starts_with($websiteUrl, 'https://')) {
|
||||
$websiteDomain = substr($websiteUrl, 8);
|
||||
} else {
|
||||
$websiteDomain = $websiteUrl;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<input type="text" name="website_url" value="{{ old('website_url', $websiteDomain) }}"
|
||||
class="input input-bordered @error('website_url') input-error @enderror"
|
||||
placeholder="example.com">
|
||||
@error('website_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Brand Phone -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Brand Phone</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Contact phone number for this brand">
|
||||
<span class="iconify lucide--info size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="tel" name="phone" value="{{ old('phone', $brand->phone) }}"
|
||||
class="input input-bordered @error('phone') input-error @enderror"
|
||||
placeholder="(555) 123-4567">
|
||||
@error('phone')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Short Description -->
|
||||
<div class="form-control mt-4 w-full">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Short Description</span>
|
||||
<span class="label-text-alt">
|
||||
<span class="tooltip tooltip-left mr-2" data-tip="Brief description shown in brand listings">
|
||||
<span class="iconify lucide--info size-4"></span>
|
||||
</span>
|
||||
<span class="text-sm" id="description-count">0/300</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea name="description"
|
||||
rows="3"
|
||||
class="textarea textarea-bordered resize-none w-full @error('description') textarea-error @enderror"
|
||||
placeholder="Brief description of the brand"
|
||||
maxlength="300"
|
||||
oninput="updateCharCount('description', 'description-count', 300)">{{ old('description', $brand->description) }}</textarea>
|
||||
@error('description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Long Description -->
|
||||
<div class="form-control mt-4 w-full">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Long Description</span>
|
||||
<span class="label-text-alt">
|
||||
<span class="tooltip tooltip-left mr-2" data-tip="Detailed brand story shown on brand preview page">
|
||||
<span class="iconify lucide--info size-4"></span>
|
||||
</span>
|
||||
<span class="text-sm" id="long-description-count">0/1000</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea name="long_description"
|
||||
rows="10"
|
||||
class="textarea textarea-bordered resize-none w-full @error('long_description') textarea-error @enderror"
|
||||
placeholder="Tell buyers about your brand's story, values, and what makes your products unique"
|
||||
maxlength="1000"
|
||||
oninput="updateCharCount('long_description', 'long-description-count', 1000)">{{ old('long_description', $brand->long_description) }}</textarea>
|
||||
@error('long_description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information Section -->
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Address Information</h2>
|
||||
|
||||
<!-- Street Address -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<input type="text" name="address" value="{{ old('address', $brand->address) }}"
|
||||
class="input input-bordered @error('address') input-error @enderror"
|
||||
placeholder="123 Main Street">
|
||||
@error('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 lg:grid-cols-4 gap-6 mt-4">
|
||||
<!-- Unit Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Unit/Suite</span>
|
||||
</label>
|
||||
<input type="text" name="unit_number" value="{{ old('unit_number', $brand->unit_number) }}"
|
||||
class="input input-bordered @error('unit_number') input-error @enderror"
|
||||
placeholder="Suite 100">
|
||||
@error('unit_number')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- City -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input type="text" name="city" value="{{ old('city', $brand->city) }}"
|
||||
class="input input-bordered @error('city') input-error @enderror"
|
||||
placeholder="Phoenix">
|
||||
@error('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>
|
||||
<input type="text" name="state" value="{{ old('state', $brand->state) }}"
|
||||
class="input input-bordered @error('state') input-error @enderror"
|
||||
placeholder="AZ" maxlength="2">
|
||||
@error('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="zip_code" value="{{ old('zip_code', $brand->zip_code) }}"
|
||||
class="input input-bordered @error('zip_code') input-error @enderror"
|
||||
placeholder="85001">
|
||||
@error('zip_code')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Images Section -->
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Brand Images</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Brand Logo -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Brand Logo</span>
|
||||
</label>
|
||||
|
||||
@if($brand->hasLogo())
|
||||
<div class="mb-3">
|
||||
<img src="{{ $brand->getLogoUrl() }}" alt="{{ $brand->name }} logo" class="w-32 h-32 rounded-lg shadow object-contain border border-base-300">
|
||||
</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 text-base-content/60">Max: 2MB | Recommended: 500x500px</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 shadow object-contain border border-base-300">
|
||||
</div>
|
||||
|
||||
@if($brand->hasLogo())
|
||||
<div class="form-control mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="remove_logo" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current logo</span>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Brand Banner -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Brand Banner</span>
|
||||
</label>
|
||||
|
||||
@if($brand->banner_path && \Storage::disk('public')->exists($brand->banner_path))
|
||||
<div class="mb-3">
|
||||
<img src="{{ asset('storage/' . $brand->banner_path) }}" alt="{{ $brand->name }} banner" class="w-full h-32 rounded-lg shadow object-cover border border-base-300">
|
||||
</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 text-base-content/60">Max: 4MB | Recommended: 1920x400px</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-32 rounded-lg shadow object-cover border border-base-300">
|
||||
</div>
|
||||
|
||||
@if($brand->banner_path && \Storage::disk('public')->exists($brand->banner_path))
|
||||
<div class="form-control mt-3">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="remove_banner" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current banner</span>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Section -->
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Social Media</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Instagram -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium inline-flex items-center gap-2">
|
||||
<span class="iconify lucide--instagram size-4 text-base-content/50"></span>
|
||||
Instagram
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<span class="join-item flex items-center bg-base-200 px-3 text-sm">@</span>
|
||||
<input type="text" name="instagram_handle" value="{{ old('instagram_handle', $brand->instagram_handle) }}"
|
||||
class="input input-bordered join-item flex-1 @error('instagram_handle') input-error @enderror"
|
||||
placeholder="yourbrand">
|
||||
</div>
|
||||
@error('instagram_handle')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Twitter -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium inline-flex items-center gap-2">
|
||||
<span class="iconify lucide--twitter size-4 text-base-content/50"></span>
|
||||
Twitter
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<span class="join-item flex items-center bg-base-200 px-3 text-sm">@</span>
|
||||
<input type="text" name="twitter_handle" value="{{ old('twitter_handle', $brand->twitter_handle) }}"
|
||||
class="input input-bordered join-item flex-1 @error('twitter_handle') input-error @enderror"
|
||||
placeholder="yourbrand">
|
||||
</div>
|
||||
@error('twitter_handle')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Facebook -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium inline-flex items-center gap-2">
|
||||
<span class="iconify lucide--facebook size-4 text-base-content/50"></span>
|
||||
Facebook
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="facebook_url" value="{{ old('facebook_url', $brand->facebook_url) }}"
|
||||
class="input input-bordered @error('facebook_url') input-error @enderror"
|
||||
placeholder="https://facebook.com/yourbrand">
|
||||
@error('facebook_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- YouTube -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium inline-flex items-center gap-2">
|
||||
<span class="iconify lucide--youtube size-4 text-base-content/50"></span>
|
||||
YouTube
|
||||
</span>
|
||||
</label>
|
||||
<input type="url" name="youtube_url" value="{{ old('youtube_url', $brand->youtube_url) }}"
|
||||
class="input input-bordered @error('youtube_url') input-error @enderror"
|
||||
placeholder="https://youtube.com/@yourbrand">
|
||||
@error('youtube_url')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visibility Settings Section -->
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Visibility Settings</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Public Menu Checkbox -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_public" value="1"
|
||||
{{ old('is_public', $brand->is_public) ? 'checked' : '' }}
|
||||
class="checkbox">
|
||||
<div>
|
||||
<span class="label-text font-medium">Public Menu</span>
|
||||
<p class="text-sm text-base-content/60">Make this brand visible to buyers in the marketplace</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
{{ old('is_active', $brand->is_active) ? 'checked' : '' }}
|
||||
class="checkbox">
|
||||
<div>
|
||||
<span class="label-text font-medium">Active</span>
|
||||
<p class="text-sm text-base-content/60">Brand is active and can be used for products</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Featured -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" name="is_featured" value="1"
|
||||
{{ old('is_featured', $brand->is_featured) ? 'checked' : '' }}
|
||||
class="checkbox">
|
||||
<div>
|
||||
<span class="label-text font-medium">Featured Brand</span>
|
||||
<p class="text-sm text-base-content/60">Highlight this brand on the marketplace</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 justify-end mb-6">
|
||||
<a href="{{ route('seller.business.brands.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="iconify lucide--save size-4 mr-2"></span>
|
||||
Update Brand
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Character count for textareas
|
||||
function updateCharCount(fieldName, counterId, maxLength) {
|
||||
const field = document.querySelector(`[name="${fieldName}"]`);
|
||||
const counter = document.getElementById(counterId);
|
||||
if (field && counter) {
|
||||
const currentLength = field.value.length;
|
||||
counter.textContent = `${currentLength}/${maxLength}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize character counts on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateCharCount('tagline', 'tagline-count', 45);
|
||||
updateCharCount('description', 'description-count', 300);
|
||||
updateCharCount('long_description', 'long-description-count', 1000);
|
||||
});
|
||||
|
||||
// Image preview functionality
|
||||
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]);
|
||||
} else {
|
||||
preview.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
147
resources/views/seller/brands/index.blade.php
Normal file
147
resources/views/seller/brands/index.blade.php
Normal file
@@ -0,0 +1,147 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Brands</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 class="opacity-80">Brands</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Brand Button -->
|
||||
<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="iconify lucide--package size-8"></span>
|
||||
Brands
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Manage brands associated with {{ $business->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('seller.business.brands.create', $business->slug) }}" class="btn btn-primary">
|
||||
<span class="iconify lucide--plus size-4.5"></span>
|
||||
Add Brand
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brands Table -->
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow overflow-visible">
|
||||
<div class="card-body p-0 overflow-visible">
|
||||
@if ($brands->isEmpty())
|
||||
<div class="p-16 text-center">
|
||||
<span class="iconify lucide--package text-base-content/30 mx-auto size-16"></span>
|
||||
<h3 class="mt-4 text-lg font-medium text-base-content">No brands</h3>
|
||||
<p class="mt-2 text-sm text-base-content/60">Get started by creating your first brand.</p>
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('seller.business.brands.create', $business->slug) }}" class="btn btn-primary">
|
||||
<span class="iconify lucide--plus size-4.5"></span>
|
||||
Create Brand
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto overflow-y-visible">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Brand Name</th>
|
||||
<th>Brand URL</th>
|
||||
<th>Status</th>
|
||||
<th>Products</th>
|
||||
<th>Visibility</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($brands as $brand)
|
||||
<tr>
|
||||
<td>
|
||||
@if ($brand->hasLogo())
|
||||
<div class="avatar">
|
||||
<div class="mask mask-squircle w-10 h-10">
|
||||
<img src="{{ $brand->getLogoUrl() }}"
|
||||
alt="{{ $brand->name }}" />
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content mask mask-squircle w-10">
|
||||
<span class="text-xl">{{ substr($brand->name, 0, 1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="font-medium">{{ $brand->name }}</div>
|
||||
@if ($brand->tagline)
|
||||
<div class="text-sm text-base-content/60">{{ $brand->tagline }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($brand->website_url)
|
||||
<a href="{{ $brand->website_url }}" target="_blank" class="link link-primary text-sm">
|
||||
{{ $brand->website_url }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-base-content/40 text-sm">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($brand->is_active)
|
||||
<div class="badge badge-ghost gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" class="inline-block h-4 w-4 stroke-current">
|
||||
<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>
|
||||
Active
|
||||
</div>
|
||||
@else
|
||||
<div class="badge badge-ghost gap-2">
|
||||
Inactive
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-sm">{{ $brand->products()->count() }} products</span>
|
||||
</td>
|
||||
<td>
|
||||
@if ($brand->is_public)
|
||||
<div class="badge badge-outline">Public</div>
|
||||
@else
|
||||
<div class="badge badge-ghost">Private</div>
|
||||
@endif
|
||||
@if ($brand->is_featured)
|
||||
<div class="badge badge-ghost">Featured</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<a href="{{ route('seller.business.brands.preview', [$business->slug, $brand]) }}" target="_blank" class="btn btn-primary btn-sm">
|
||||
<span class="iconify lucide--eye size-4"></span>
|
||||
Preview
|
||||
</a>
|
||||
<a href="{{ route('seller.business.brands.edit', [$business->slug, $brand]) }}" class="btn btn-outline btn-sm">
|
||||
<span class="iconify lucide--pencil size-4"></span>
|
||||
Edit Brand
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
545
resources/views/seller/brands/preview.blade.php
Normal file
545
resources/views/seller/brands/preview.blade.php
Normal file
@@ -0,0 +1,545 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="breadcrumbs text-sm mb-6">
|
||||
<ul>
|
||||
@if($isSeller)
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.brands', $business->slug) }}">Brands</a></li>
|
||||
<li class="opacity-60">{{ $brand->name }}</li>
|
||||
@else
|
||||
<li><a href="{{ route('buyer.dashboard') }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('buyer.browse') }}">Browse</a></li>
|
||||
<li class="opacity-60">{{ $brand->name }}</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@if($isSeller)
|
||||
<!-- Context Banner (Seller Only) -->
|
||||
<div class="alert bg-info/10 border-info/20 mb-6">
|
||||
<span class="iconify lucide--eye size-5 text-info"></span>
|
||||
<span class="text-sm">Below is a preview of how your menu appears to retailers when they shop <span class="font-semibold">{{ $brand->name }}</span>.</span>
|
||||
</div>
|
||||
|
||||
<!-- Preview Menu Header (Seller Only) -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold mb-1">Preview Menu</h1>
|
||||
<p class="text-base-content/70">Edit product order and preview your menu</p>
|
||||
</div>
|
||||
|
||||
<!-- General Information and Menu Checklist (Seller Only) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- General Information -->
|
||||
<div class="bg-base-100 border border-base-300 p-6">
|
||||
<h2 class="text-lg font-semibold mb-4">General Information</h2>
|
||||
<div class="space-y-4 text-sm text-base-content/80">
|
||||
<p>Your menu is what retailers see when shopping {{ $brand->name }}—let's make it shine.</p>
|
||||
|
||||
<p>Areas highlighted in red show recommended updates to help your products stand out and attract more retailer interest. These tips come directly from feedback we've received from buyers and are designed to make your listings even more effective. Don't worry—these notes are <span class="text-error font-medium">only visible to you</span> and never shown to retailers.</p>
|
||||
|
||||
<p>Looking for personalized suggestions or extra support? Our Client Experience Team is always happy to help you get the most out of your menu. Reach out anytime at <a href="mailto:support@cannabrands.com" class="link link-primary">support@cannabrands.com</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Checklist -->
|
||||
<div class="bg-base-100 border border-base-300 p-6">
|
||||
<h2 class="text-lg font-semibold mb-4">Menu Checklist</h2>
|
||||
<p class="text-sm text-base-content/80 mb-4">Let's make sure your menu is ready to impress!</p>
|
||||
<ul class="space-y-3 text-sm text-base-content/80">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="iconify lucide--check-circle size-5 text-success mt-0.5 flex-shrink-0"></span>
|
||||
<span>Are all your products showing up the way you expect?</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="iconify lucide--check-circle size-5 text-success mt-0.5 flex-shrink-0"></span>
|
||||
<span>Do each of your products have great images and clear descriptions?</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="iconify lucide--check-circle size-5 text-success mt-0.5 flex-shrink-0"></span>
|
||||
<span>Are your items organized by product line? (That's especially helpful for bigger menus!)</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="iconify lucide--check-circle size-5 text-success mt-0.5 flex-shrink-0"></span>
|
||||
<span>Have you double-checked your sample requests and min/max order quantities?</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Menu Button (Seller Only) -->
|
||||
<div class="flex justify-end mb-6">
|
||||
<button class="btn btn-primary gap-2" onclick="share_menu_modal.showModal()">
|
||||
<span class="iconify lucide--share-2 size-4"></span>
|
||||
Share Menu
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="relative w-full mb-8 rounded overflow-hidden" style="height: 300px;">
|
||||
@if($brand->banner_path)
|
||||
<!-- Brand Banner Image -->
|
||||
<img src="{{ asset('storage/' . $brand->banner_path) }}"
|
||||
alt="{{ $brand->name }} banner"
|
||||
class="w-full h-full object-cover">
|
||||
|
||||
<!-- Overlay with brand logo and name -->
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-black/20">
|
||||
<div class="text-center">
|
||||
@if($brand->hasLogo())
|
||||
<div class="mb-4">
|
||||
<img src="{{ $brand->getLogoUrl() }}"
|
||||
alt="{{ $brand->name }}"
|
||||
class="max-h-24 mx-auto drop-shadow-2xl">
|
||||
</div>
|
||||
@endif
|
||||
<h1 class="text-5xl font-bold text-white drop-shadow-2xl" style="text-shadow: 2px 2px 8px rgba(0,0,0,0.8);">
|
||||
{{ $brand->name }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Fallback: No banner image -->
|
||||
<div class="w-full h-full bg-gradient-to-r from-primary/20 to-secondary/20 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
@if($brand->hasLogo())
|
||||
<div class="mb-4">
|
||||
<img src="{{ $brand->getLogoUrl() }}"
|
||||
alt="{{ $brand->name }}"
|
||||
class="max-h-24 mx-auto">
|
||||
</div>
|
||||
@endif
|
||||
<h1 class="text-5xl font-bold text-base-content">
|
||||
{{ $brand->name }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Brand Information Section -->
|
||||
<div class="bg-base-100 border border-base-300 mb-8">
|
||||
<div class="p-8">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<!-- Brand Details -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-4 mb-3">
|
||||
<div>
|
||||
@if($brand->tagline)
|
||||
<p class="text-xl text-base-content/70 mb-2">{{ $brand->tagline }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs font-medium border border-base-300 bg-base-50">
|
||||
{{ strtoupper($brand->business->license_type ?? 'MED') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if($brand->description)
|
||||
<p class="text-sm text-base-content/80 mb-4 leading-relaxed">{{ $brand->description }}</p>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap gap-3 mb-4">
|
||||
@if($brand->website_url)
|
||||
<a href="{{ $brand->website_url }}" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-base-200/50 hover:bg-base-300/50 border border-base-300 rounded-lg transition-colors group">
|
||||
<span class="iconify lucide--globe size-4 text-base-content/50 group-hover:text-base-content/70"></span>
|
||||
<span class="text-base-content/70 group-hover:text-base-content">{{ parse_url($brand->website_url, PHP_URL_HOST) }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($brand->instagram_handle)
|
||||
<a href="https://instagram.com/{{ $brand->instagram_handle }}" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-base-200/50 hover:bg-base-300/50 border border-base-300 rounded-lg transition-colors group">
|
||||
<span class="iconify lucide--instagram size-4 text-base-content/50 group-hover:text-base-content/70"></span>
|
||||
<span class="text-base-content/70 group-hover:text-base-content">@{{ $brand->instagram_handle }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- About the Company Button -->
|
||||
<button class="btn btn-sm btn-outline gap-2" onclick="about_company_modal.showModal()">
|
||||
<span class="iconify lucide--building-2 size-4"></span>
|
||||
About the Company
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Seller Sidebar -->
|
||||
<div class="lg:w-64 flex-shrink-0 border-t lg:border-t-0 lg:border-l border-base-300 pt-6 lg:pt-0 lg:pl-8">
|
||||
<div class="text-sm mb-4">
|
||||
<p class="text-base-content/60 mb-1">Distributed by</p>
|
||||
<p class="font-medium">{{ $brand->business->name }}</p>
|
||||
</div>
|
||||
|
||||
@if($otherBrands->count() > 0)
|
||||
<div class="mt-6">
|
||||
<label class="block text-sm text-base-content/60 mb-2">Other brands from this seller</label>
|
||||
<select class="select select-bordered select-sm w-full"
|
||||
onchange="if(this.value) window.location.href=this.value">
|
||||
<option value="">Select a brand</option>
|
||||
@foreach($otherBrands as $otherBrand)
|
||||
@if($isSeller)
|
||||
<option value="{{ route('seller.business.brands.preview', [$business->slug, $otherBrand->slug]) }}">
|
||||
{{ $otherBrand->name }}
|
||||
</option>
|
||||
@else
|
||||
<option value="{{ route('buyer.brands.browse', [$business->slug, $otherBrand->slug]) }}">
|
||||
{{ $otherBrand->name }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Section -->
|
||||
@if($productsByLine->count() > 0)
|
||||
@foreach($productsByLine as $lineName => $lineProducts)
|
||||
<div class="bg-base-100 border border-base-300 mb-6">
|
||||
<div class="border-b border-base-300 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold">{{ $lineName }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr class="border-base-300">
|
||||
<th class="bg-base-50">Product</th>
|
||||
<th class="bg-base-50">Type</th>
|
||||
<th class="bg-base-50 text-right">Price</th>
|
||||
<th class="bg-base-50 text-center">Availability</th>
|
||||
<th class="bg-base-50 text-center">QTY</th>
|
||||
@if(!$isSeller)
|
||||
<th class="bg-base-50"></th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($lineProducts as $product)
|
||||
<tr class="border-base-300 hover:bg-base-50">
|
||||
<td class="py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 flex-shrink-0 border border-base-300 bg-base-50">
|
||||
@if($product->images && $product->images->first())
|
||||
<img src="{{ $product->images->first()->getUrl() }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<span class="iconify lucide--package size-5 text-base-content/20"></span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-sm">{{ $product->name }}</div>
|
||||
<div class="text-xs text-base-content/50">{{ $product->sku }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($product->strain)
|
||||
<span class="text-xs px-2 py-1 border border-base-300 bg-base-50">
|
||||
{{ ucfirst($product->strain->classification ?? 'N/A') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-base-content/30">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="font-semibold">${{ number_format($product->price ?? 0, 2) }}</div>
|
||||
@if($product->unit)
|
||||
<div class="text-xs text-base-content/50">per {{ $product->unit->name }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if($product->quantity_available > 0)
|
||||
<span class="text-xs text-base-content/70">
|
||||
{{ $product->quantity_available }} units
|
||||
</span>
|
||||
@else
|
||||
<span class="text-xs text-base-content/40">Out of stock</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if($isSeller)
|
||||
<!-- Disabled QTY input for sellers -->
|
||||
<input type="number"
|
||||
class="input input-bordered input-sm w-20 text-center"
|
||||
value="0"
|
||||
min="0"
|
||||
disabled>
|
||||
@else
|
||||
<!-- Active QTY input for buyers -->
|
||||
<input type="number"
|
||||
class="input input-bordered input-sm w-20 text-center"
|
||||
value="0"
|
||||
min="0"
|
||||
max="{{ $product->quantity_available }}"
|
||||
data-product-id="{{ $product->id }}">
|
||||
@endif
|
||||
</td>
|
||||
@if(!$isSeller)
|
||||
<td class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<!-- Sample Button -->
|
||||
<button class="btn btn-sm btn-ghost gap-1"
|
||||
title="Request Sample"
|
||||
onclick="requestSample({{ $product->id }})">
|
||||
<span class="iconify lucide--flask-conical size-4"></span>
|
||||
Sample
|
||||
</button>
|
||||
<!-- Message Button -->
|
||||
<button class="btn btn-sm btn-ghost gap-1"
|
||||
title="Message Seller"
|
||||
onclick="messageSeller({{ $product->id }})">
|
||||
<span class="iconify lucide--message-circle size-4"></span>
|
||||
Message
|
||||
</button>
|
||||
<!-- Add to Cart Button -->
|
||||
<button class="btn btn-sm btn-primary gap-2"
|
||||
onclick="addToCart({{ $product->id }})">
|
||||
<span class="iconify lucide--shopping-cart size-4"></span>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="bg-base-100 border border-base-300">
|
||||
<div class="text-center py-16">
|
||||
<span class="iconify lucide--package size-12 text-base-content/20 mb-4 block"></span>
|
||||
<h3 class="text-base font-medium text-base-content mb-2">No products available</h3>
|
||||
<p class="text-sm text-base-content/60">This brand doesn't have any products listed yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Share Menu Modal (Seller Only) -->
|
||||
@if($isSeller)
|
||||
<dialog id="share_menu_modal" class="modal">
|
||||
<div class="modal-box max-w-lg">
|
||||
<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">Share Menu</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Copy Link -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text">Share Link</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text"
|
||||
id="share_link"
|
||||
class="input input-bordered flex-1"
|
||||
value="{{ route('buyer.brands.browse', [$business->slug, $brand->slug]) }}"
|
||||
readonly>
|
||||
<button class="btn btn-primary" onclick="copyShareLink()">
|
||||
<span class="iconify lucide--copy size-4"></span>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Share -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text">Share via Email</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="email"
|
||||
id="share_email"
|
||||
class="input input-bordered flex-1"
|
||||
placeholder="buyer@example.com">
|
||||
<button class="btn btn-primary" onclick="shareViaEmail()">
|
||||
<span class="iconify lucide--mail size-4"></span>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Menu -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text">Download Menu</span>
|
||||
</label>
|
||||
<button class="btn btn-outline w-full" onclick="downloadMenu()">
|
||||
<span class="iconify lucide--download size-4"></span>
|
||||
Download as PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@endif
|
||||
|
||||
<!-- About the Company Modal (Both Views) -->
|
||||
<dialog id="about_company_modal" 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">About {{ $brand->business->name }}</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Company Logo -->
|
||||
@if($brand->business->logo_path)
|
||||
<div class="flex justify-center mb-4">
|
||||
<img src="{{ asset($brand->business->logo_path) }}"
|
||||
alt="{{ $brand->business->name }}"
|
||||
class="max-h-24 object-contain">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Company Description -->
|
||||
@if($brand->business->description)
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">About</h4>
|
||||
<p class="text-sm text-base-content/80 leading-relaxed">{{ $brand->business->description }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- License Information -->
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">License Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
@if($brand->business->license_number)
|
||||
<div>
|
||||
<span class="text-base-content/60">License Number</span>
|
||||
<p class="font-medium">{{ $brand->business->license_number }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if($brand->business->license_type)
|
||||
<div>
|
||||
<span class="text-base-content/60">License Type</span>
|
||||
<p class="font-medium">{{ strtoupper($brand->business->license_type) }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">Contact Information</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
@if($brand->business->physical_address)
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="iconify lucide--map-pin size-4 mt-0.5 text-base-content/60"></span>
|
||||
<span>
|
||||
{{ $brand->business->physical_address }}
|
||||
@if($brand->business->physical_city || $brand->business->physical_state || $brand->business->physical_zipcode)
|
||||
<br>{{ $brand->business->physical_city }}@if($brand->business->physical_city && $brand->business->physical_state), @endif{{ $brand->business->physical_state }} {{ $brand->business->physical_zipcode }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($brand->business->business_phone)
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="iconify lucide--phone size-4 text-base-content/60"></span>
|
||||
<span>{{ $brand->business->business_phone }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($brand->business->business_email)
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="iconify lucide--mail size-4 text-base-content/60"></span>
|
||||
<a href="mailto:{{ $brand->business->business_email }}" class="hover:underline">
|
||||
{{ $brand->business->business_email }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brands from this Company -->
|
||||
@if($otherBrands->count() > 0 || true)
|
||||
<div>
|
||||
<h4 class="font-semibold mb-2">Brands</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="badge badge-lg">{{ $brand->name }}</span>
|
||||
@foreach($otherBrands as $otherBrand)
|
||||
<a href="@if($isSeller){{ route('seller.business.brands.preview', [$business->slug, $otherBrand->slug]) }}@else{{ route('buyer.brands.browse', [$business->slug, $otherBrand->slug]) }}@endif"
|
||||
class="badge badge-lg badge-outline hover:badge-primary">
|
||||
{{ $otherBrand->name }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Share Menu Functions
|
||||
function copyShareLink() {
|
||||
const link = document.getElementById('share_link');
|
||||
link.select();
|
||||
document.execCommand('copy');
|
||||
alert('Link copied to clipboard!');
|
||||
}
|
||||
|
||||
function shareViaEmail() {
|
||||
const email = document.getElementById('share_email').value;
|
||||
if (!email) {
|
||||
alert('Please enter an email address');
|
||||
return;
|
||||
}
|
||||
// TODO: Implement email sharing functionality
|
||||
alert('Email functionality coming soon!');
|
||||
}
|
||||
|
||||
function downloadMenu() {
|
||||
// TODO: Implement PDF download functionality
|
||||
alert('PDF download coming soon!');
|
||||
}
|
||||
|
||||
// Buyer Functions
|
||||
@if(!$isSeller)
|
||||
function requestSample(productId) {
|
||||
// TODO: Implement sample request functionality
|
||||
alert('Sample request for product ' + productId);
|
||||
}
|
||||
|
||||
function messageSeller(productId) {
|
||||
// TODO: Implement messaging functionality
|
||||
alert('Message seller about product ' + productId);
|
||||
}
|
||||
|
||||
function addToCart(productId) {
|
||||
const qtyInput = document.querySelector(`input[data-product-id="${productId}"]`);
|
||||
const quantity = parseInt(qtyInput.value) || 0;
|
||||
|
||||
if (quantity <= 0) {
|
||||
alert('Please enter a quantity');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement add to cart functionality
|
||||
alert(`Added ${quantity} units of product ${productId} to cart`);
|
||||
}
|
||||
@endif
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
0
resources/views/seller/brands/show.blade.php
Normal file
0
resources/views/seller/brands/show.blade.php
Normal file
@@ -3,8 +3,8 @@
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
{{-- Top Header Bar --}}
|
||||
<div class="bg-white border border-gray-300 rounded-md shadow-sm mb-6">
|
||||
<div class="px-6 py-4">
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between gap-6">
|
||||
{{-- Left: Product Image & Info --}}
|
||||
<div class="flex items-start gap-4">
|
||||
@@ -13,10 +13,10 @@
|
||||
@if($product->images->where('is_primary', true)->first())
|
||||
<img src="{{ asset('storage/' . $product->images->where('is_primary', true)->first()->path) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-16 h-16 object-cover rounded-md border border-gray-300">
|
||||
class="w-16 h-16 object-cover rounded-md border border-base-300">
|
||||
@else
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-md border border-gray-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="w-16 h-16 bg-base-200 rounded-md border border-base-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
{{-- Product Details --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h1 class="text-xl font-bold text-gray-900">{{ $product->name }}</h1>
|
||||
<h1 class="text-xl font-bold text-base-content">{{ $product->name }}</h1>
|
||||
{{-- Status Badges --}}
|
||||
<span id="activeBadge" style="display: {{ $product->is_active ? 'inline-flex' : 'none' }};" class="items-center px-2.5 py-0.5 rounded text-xs font-medium bg-success text-white">
|
||||
Active
|
||||
@@ -35,39 +35,30 @@
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-0.5">
|
||||
<div class="text-sm text-base-content/70 space-y-0.5">
|
||||
<div><span class="font-medium">SKU:</span> <span class="font-mono">{{ $product->sku ?? 'N/A' }}</span> <span class="mx-2">•</span> <span class="font-medium">Brand:</span> {{ $product->brand->name ?? 'N/A' }}</div>
|
||||
<div><span class="font-medium">Last updated:</span> {{ $product->updated_at->format('M j, Y g:i A') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Right: Action Buttons & Breadcrumb --}}
|
||||
{{-- Right: Action Buttons --}}
|
||||
<div class="flex flex-col gap-2 flex-shrink-0">
|
||||
{{-- View on Marketplace Button (White with border) --}}
|
||||
<a href="#" target="_blank" class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- View on Marketplace Button --}}
|
||||
<a href="#" target="_blank" class="btn btn-outline btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
View on Marketplace
|
||||
</a>
|
||||
|
||||
{{-- Manage BOM Button (Blue solid) --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-primary hover:bg-primary/90 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- Manage BOM Button --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="btn btn-primary btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||
</svg>
|
||||
Manage BOM
|
||||
</a>
|
||||
|
||||
{{-- Breadcrumb Navigation --}}
|
||||
<nav class="flex text-xs text-gray-500 mt-1" aria-label="Breadcrumb">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="hover:text-gray-700">Dashboard</a>
|
||||
<span class="mx-2">></span>
|
||||
<a href="{{ route('seller.business.products.index', $business->slug) }}" class="hover:text-gray-700">Products</a>
|
||||
<span class="mx-2">></span>
|
||||
<span class="text-gray-900 font-medium">Edit</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +95,7 @@
|
||||
{{-- LEFT SIDEBAR (1/4 width) --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Product Images Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Product Images</h2>
|
||||
<div class="space-y-4">
|
||||
@@ -147,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Quick Stats Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Quick Stats</h2>
|
||||
<div class="space-y-2">
|
||||
@@ -173,7 +164,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Audit Info Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xs">Audit Info</h2>
|
||||
<div class="space-y-1 text-xs text-base-content/70">
|
||||
@@ -186,7 +177,7 @@
|
||||
|
||||
{{-- MAIN CONTENT WITH TABS (3/4 width) --}}
|
||||
<div class="lg:col-span-3">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{{-- Tabs Navigation --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
236
resources/views/seller/settings/audit-logs.blade.php
Normal file
236
resources/views/seller/settings/audit-logs.blade.php
Normal file
@@ -0,0 +1,236 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--scroll-text] size-6"></span>
|
||||
<p class="text-lg font-medium">Audit Logs</p>
|
||||
</div>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-60">Audit Logs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Audit Logs Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-[lucide--scroll-text] size-6 text-primary"></span>
|
||||
<h2 class="text-lg font-semibold">System Activity</h2>
|
||||
</div>
|
||||
|
||||
<!-- Filter and Search Controls -->
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm btn-outline gap-2">
|
||||
<span class="icon-[lucide--filter] size-4"></span>
|
||||
Add Filter
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="mb-4">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="icon-[lucide--search] size-4 text-base-content/40"></span>
|
||||
<input type="text" class="grow" placeholder="Search audit logs..." id="auditSearch" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Audit Logs Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center gap-1 cursor-pointer hover:text-primary">
|
||||
Timestamp
|
||||
<span class="icon-[lucide--arrow-up-down] size-3"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Resource</th>
|
||||
<th>IP Address</th>
|
||||
<th>User Agent</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($audits as $audit)
|
||||
<tr class="hover">
|
||||
<td class="text-sm">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ $audit->created_at->format('M d, Y') }}</span>
|
||||
<span class="text-xs text-base-content/60">{{ $audit->created_at->format('h:i:s A') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-sm">{{ $audit->user?->name ?? 'System' }}</span>
|
||||
<span class="text-xs text-base-content/60">{{ $audit->user?->email ?? 'system@automated' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($audit->event === 'created')
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<span class="icon-[lucide--plus-circle] size-3"></span>
|
||||
Created
|
||||
</span>
|
||||
@elseif($audit->event === 'updated')
|
||||
<span class="badge badge-info badge-sm gap-1">
|
||||
<span class="icon-[lucide--pencil] size-3"></span>
|
||||
Updated
|
||||
</span>
|
||||
@elseif($audit->event === 'deleted')
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<span class="icon-[lucide--trash-2] size-3"></span>
|
||||
Deleted
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-ghost badge-sm">{{ ucfirst($audit->event) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-sm">{{ class_basename($audit->auditable_type) }}</span>
|
||||
<span class="text-xs text-base-content/60">#{{ $audit->auditable_id }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="font-mono text-xs">{{ $audit->ip_address ?? 'N/A' }}</td>
|
||||
<td class="max-w-xs">
|
||||
<div class="tooltip tooltip-left" data-tip="{{ $audit->user_agent ?? 'N/A' }}">
|
||||
<span class="text-xs text-base-content/60 truncate block">
|
||||
{{ Str::limit($audit->user_agent ?? 'N/A', 30) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="viewAuditDetails{{ $audit->id }}.showModal()" class="btn btn-ghost btn-sm btn-square">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Audit Details Modal -->
|
||||
<dialog id="viewAuditDetails{{ $audit->id }}" class="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<span class="icon-[lucide--file-text] size-5"></span>
|
||||
Audit Details
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">Timestamp</label>
|
||||
<p class="text-sm">{{ $audit->created_at->format('F d, Y \a\t h:i:s A') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">Action</label>
|
||||
<p class="text-sm">{{ ucfirst($audit->event) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">User</label>
|
||||
<p class="text-sm">{{ $audit->user?->name ?? 'System' }}</p>
|
||||
<p class="text-xs text-base-content/60">{{ $audit->user?->email ?? 'system@automated' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60">IP Address</label>
|
||||
<p class="text-sm font-mono">{{ $audit->ip_address ?? 'N/A' }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-semibold text-base-content/60">Resource</label>
|
||||
<p class="text-sm">{{ class_basename($audit->auditable_type) }} #{{ $audit->auditable_id }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-semibold text-base-content/60">User Agent</label>
|
||||
<p class="text-xs font-mono break-all">{{ $audit->user_agent ?? 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Changes -->
|
||||
@if($audit->old_values || $audit->new_values)
|
||||
<div class="divider"></div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-base-content/60 mb-2 block">Changes</label>
|
||||
<div class="space-y-2">
|
||||
@foreach(array_keys(array_merge($audit->old_values ?? [], $audit->new_values ?? [])) as $field)
|
||||
<div class="bg-base-200 p-3 rounded-lg">
|
||||
<div class="font-semibold text-sm mb-1">{{ ucfirst($field) }}</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span class="text-base-content/60">Old Value:</span>
|
||||
<div class="bg-error/10 text-error p-2 rounded mt-1 font-mono">{{ $audit->old_values[$field] ?? 'N/A' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">New Value:</span>
|
||||
<div class="bg-success/10 text-success p-2 rounded mt-1 font-mono">{{ $audit->new_values[$field] ?? 'N/A' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-8 text-base-content/60">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="icon-[lucide--inbox] size-12 text-base-content/20"></span>
|
||||
<p>No audit logs found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($audits->hasPages())
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div class="text-sm text-base-content/60">
|
||||
Showing {{ $audits->firstItem() ?? 0 }} to {{ $audits->lastItem() ?? 0 }} of {{ $audits->total() }} entries
|
||||
</div>
|
||||
<div>
|
||||
{{ $audits->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple client-side search functionality
|
||||
document.getElementById('auditSearch').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
336
resources/views/seller/settings/brand-kit.blade.php
Normal file
336
resources/views/seller/settings/brand-kit.blade.php
Normal file
@@ -0,0 +1,336 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Brand Kit</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.company-information', $business->slug) }}">Company</a></li>
|
||||
<li class="opacity-80">Brand Kit</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header with Actions -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content flex items-center gap-2">
|
||||
<span class="icon-[lucide--palette] size-8"></span>
|
||||
Cannabrands Brand Kit
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Download brand assets for Cannabrands</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-outline btn-sm gap-2">
|
||||
<span class="icon-[lucide--upload] size-4"></span>
|
||||
Upload
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Logos Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--image] size-5 text-primary"></span>
|
||||
Logos
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands logos in various formats
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Primary Logo SVG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-white rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-primary.svg') }}"
|
||||
alt="Cannabrands Primary Logo"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsLogo%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Primary Logo</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">SVG Format</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-primary.svg') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Logo PNG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-white rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-primary.png') }}"
|
||||
alt="Cannabrands Primary Logo PNG"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsLogo%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Primary Logo</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">PNG Format (High-Res)</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-primary.png') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Logo SVG -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-center bg-base-300 rounded-lg p-6 mb-3 min-h-[120px]">
|
||||
<img src="{{ asset('storage/brand-kit/cannabrands/logo-secondary.svg') }}"
|
||||
alt="Cannabrands Secondary Logo"
|
||||
class="max-h-20"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%2250%22%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-family=%22Arial%22 font-size=%2214%22 fill=%22%23666%22%3ECannabrandsIcon%3C/text%3E%3C/svg%3E'">
|
||||
</div>
|
||||
<h3 class="font-medium text-sm">Icon/Mark</h3>
|
||||
<p class="text-xs text-base-content/60 mb-3">SVG Format</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
View
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/logo-secondary.svg') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colors Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--palette] size-5 text-primary"></span>
|
||||
Colors
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands brand colors with hex codes
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@php
|
||||
$colors = [
|
||||
['name' => 'Primary Green', 'hex' => '#10B981', 'description' => 'Main brand color'],
|
||||
['name' => 'Dark Green', 'hex' => '#047857', 'description' => 'Secondary'],
|
||||
['name' => 'Light Green', 'hex' => '#D1FAE5', 'description' => 'Backgrounds'],
|
||||
['name' => 'Charcoal', 'hex' => '#1F2937', 'description' => 'Text primary'],
|
||||
['name' => 'Gray', 'hex' => '#6B7280', 'description' => 'Text secondary'],
|
||||
['name' => 'White', 'hex' => '#FFFFFF', 'description' => 'Backgrounds'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@foreach($colors as $color)
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-3">
|
||||
<div class="rounded-lg mb-2 h-20 border border-base-300"
|
||||
style="background-color: {{ $color['hex'] }};">
|
||||
</div>
|
||||
<h3 class="font-medium text-xs">{{ $color['name'] }}</h3>
|
||||
<p class="text-xs font-mono text-base-content/80">{{ $color['hex'] }}</p>
|
||||
<p class="text-xs text-base-content/50">{{ $color['description'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/colors.json') }}"
|
||||
download
|
||||
class="btn btn-sm btn-primary gap-2">
|
||||
<span class="icon-[lucide--download] size-4"></span>
|
||||
Download Color Palette (JSON)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typography Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--type] size-5 text-primary"></span>
|
||||
Typography
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Official Cannabrands font files and usage guidelines
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Headings Font -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">Inter Bold</h3>
|
||||
<p class="text-sm text-base-content/60">Headings & Display Text</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Bold.ttf') }}"
|
||||
download
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
TTF
|
||||
</a>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Bold.woff') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
WOFF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold">The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Font -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg">Inter Regular</h3>
|
||||
<p class="text-sm text-base-content/60">Body Text & Paragraphs</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Regular.ttf') }}"
|
||||
download
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
TTF
|
||||
</a>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/fonts/Inter-Regular.woff') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
WOFF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base">The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Section -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center gap-2">
|
||||
<span class="icon-[lucide--layout-template] size-5 text-primary"></span>
|
||||
Templates
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
Ready-to-use templates for marketing materials
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Email Template -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="rounded-lg bg-primary/10 p-3">
|
||||
<span class="icon-[lucide--mail] size-6 text-primary"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-medium">Email Template</h3>
|
||||
<p class="text-xs text-base-content/60">HTML email template</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
Preview
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/templates/email-template.html') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Template -->
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="rounded-lg bg-primary/10 p-3">
|
||||
<span class="icon-[lucide--share-2] size-6 text-primary"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-medium">Social Media</h3>
|
||||
<p class="text-xs text-base-content/60">Instagram & Facebook posts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-xs btn-outline flex-1 gap-1">
|
||||
<span class="icon-[lucide--eye] size-3"></span>
|
||||
Preview
|
||||
</button>
|
||||
<a href="{{ asset('storage/brand-kit/cannabrands/templates/social-media.zip') }}"
|
||||
download
|
||||
class="btn btn-xs btn-primary flex-1 gap-1">
|
||||
<span class="icon-[lucide--download] size-3"></span>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Guidelines Note -->
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div>
|
||||
<h3 class="font-bold">Brand Guidelines</h3>
|
||||
<div class="text-sm">
|
||||
Please follow Cannabrands brand guidelines when using these assets.
|
||||
For questions or custom requests, contact the marketing team.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Preview modal functionality can be added here
|
||||
console.log('Brand Kit page loaded');
|
||||
</script>
|
||||
@endpush
|
||||
136
resources/views/seller/settings/categories/create.blade.php
Normal file
136
resources/views/seller/settings/categories/create.blade.php
Normal file
@@ -0,0 +1,136 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Create {{ ucfirst($type) }} Category</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.categories.index', $business->slug) }}">Categories</a></li>
|
||||
<li class="opacity-80">Create</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Create {{ ucfirst($type) }} Category</h1>
|
||||
<p class="text-base-content/60 mt-1">Add a new {{ $type }} category for organizing your {{ $type }}s</p>
|
||||
</div>
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--arrow-left] size-4"></span>
|
||||
Back to Categories
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Create Form -->
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<form method="POST" action="{{ route('seller.business.settings.categories.store', [$business->slug, $type]) }}" enctype="multipart/form-data" class="card-body">
|
||||
@csrf
|
||||
|
||||
<!-- Category Image -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Category Image</span>
|
||||
</label>
|
||||
<input type="file" name="image" accept="image/*" class="file-input file-input-bordered w-full" />
|
||||
@error('image')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" class="input input-bordered w-full" required autofocus />
|
||||
@error('name')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Parent Category -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Parent</span>
|
||||
</label>
|
||||
<select name="parent_id" class="select select-bordered w-full">
|
||||
<option value="">Top-level Category</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" {{ old('parent_id', request('parent')) == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
</option>
|
||||
@if($cat->children && $cat->children->count() > 0)
|
||||
@foreach($cat->children as $child)
|
||||
<option value="{{ $child->id }}" {{ old('parent_id', request('parent')) == $child->id ? 'selected' : '' }}>
|
||||
— {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Leave blank to create a top-level category</span>
|
||||
</label>
|
||||
@error('parent_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea name="description" rows="4" class="textarea textarea-bordered w-full">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Sort Order</span>
|
||||
</label>
|
||||
<input type="number" name="sort_order" value="{{ old('sort_order', 0) }}" min="0" class="input input-bordered w-full" />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Lower numbers appear first</span>
|
||||
</label>
|
||||
@error('sort_order')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Active Status</span>
|
||||
</label>
|
||||
<div>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="is_active" value="1" class="checkbox" {{ old('is_active', true) ? 'checked' : '' }} />
|
||||
<span class="label-text">Active</span>
|
||||
</label>
|
||||
<label class="label pt-0">
|
||||
<span class="label-text-alt">Inactive categories are hidden from selection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Category
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
151
resources/views/seller/settings/categories/edit.blade.php
Normal file
151
resources/views/seller/settings/categories/edit.blade.php
Normal file
@@ -0,0 +1,151 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-lg font-medium">Edit {{ ucfirst($type) }} Category</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a href="{{ route('seller.business.settings.categories.index', $business->slug) }}">Categories</a></li>
|
||||
<li class="opacity-80">Edit</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Edit {{ ucfirst($type) }} Category</h1>
|
||||
<p class="text-base-content/60 mt-1">Update category information</p>
|
||||
</div>
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--arrow-left] size-4"></span>
|
||||
Back to Categories
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<form method="POST" action="{{ route('seller.business.settings.categories.update', [$business->slug, $type, $category->id]) }}" enctype="multipart/form-data" class="card-body">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Current Image -->
|
||||
@if($category->image_path)
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Image</span>
|
||||
</label>
|
||||
<div class="avatar">
|
||||
<div class="w-24 rounded">
|
||||
<img src="{{ asset('storage/' . $category->image_path) }}" alt="{{ $category->name }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Category Image -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ $category->image_path ? 'Replace' : 'Upload' }} Category Image</span>
|
||||
</label>
|
||||
<input type="file" name="image" accept="image/*" class="file-input file-input-bordered w-full" />
|
||||
@error('image')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $category->name) }}" class="input input-bordered w-full" required autofocus />
|
||||
@error('name')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Parent Category -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Parent</span>
|
||||
</label>
|
||||
<select name="parent_id" class="select select-bordered w-full">
|
||||
<option value="">Top-level Category</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" {{ old('parent_id', $category->parent_id) == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
</option>
|
||||
@if($cat->children && $cat->children->count() > 0)
|
||||
@foreach($cat->children as $child)
|
||||
<option value="{{ $child->id }}" {{ old('parent_id', $category->parent_id) == $child->id ? 'selected' : '' }}>
|
||||
— {{ $child->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Leave blank for a top-level category</span>
|
||||
</label>
|
||||
@error('parent_id')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea name="description" rows="4" class="textarea textarea-bordered w-full">{{ old('description', $category->description) }}</textarea>
|
||||
@error('description')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Sort Order -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Sort Order</span>
|
||||
</label>
|
||||
<input type="number" name="sort_order" value="{{ old('sort_order', $category->sort_order ?? 0) }}" min="0" class="input input-bordered w-full" />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Lower numbers appear first</span>
|
||||
</label>
|
||||
@error('sort_order')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Active Status</span>
|
||||
</label>
|
||||
<div>
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" name="is_active" value="1" class="checkbox" {{ old('is_active', $category->is_active) ? 'checked' : '' }} />
|
||||
<span class="label-text">Active</span>
|
||||
</label>
|
||||
<label class="label pt-0">
|
||||
<span class="label-text-alt">Inactive categories are hidden from selection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<a href="{{ route('seller.business.settings.categories.index', $business->slug) }}" class="btn btn-ghost">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Update Category
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
142
resources/views/seller/settings/categories/index.blade.php
Normal file
142
resources/views/seller/settings/categories/index.blade.php
Normal file
@@ -0,0 +1,142 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Categories</p>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Settings</a></li>
|
||||
<li class="opacity-80">Categories</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content flex items-center gap-2">
|
||||
<span class="icon-[lucide--folder-tree] size-8"></span>
|
||||
Categories
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Product and component categories for {{ $business->name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
@if(session('success'))
|
||||
<div class="alert mb-6">
|
||||
<span class="icon-[lucide--check-circle] size-5"></span>
|
||||
<span>{{ session('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert mb-6">
|
||||
<span class="icon-[lucide--alert-circle] size-5"></span>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Categories Tables -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Product Categories -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<span class="icon-[lucide--package] size-6"></span>
|
||||
Product Categories
|
||||
</h2>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'product']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Category
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($productCategories->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<span class="icon-[lucide--folder-x] size-12 text-base-content/20 mx-auto block mb-2"></span>
|
||||
<p class="text-base-content/60 mb-4">No product categories found</p>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'product']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Create First Category
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-right">Products</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($productCategories as $category)
|
||||
<x-category-tree-item :category="$category" type="product" :business="$business" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-base-content/60">
|
||||
Total: {{ $productCategories->count() }} top-level categories
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Component Categories -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="card-title text-xl">
|
||||
<span class="icon-[lucide--box] size-6"></span>
|
||||
Component Categories
|
||||
</h2>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'component']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Category
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if ($componentCategories->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<span class="icon-[lucide--folder-x] size-12 text-base-content/20 mx-auto block mb-2"></span>
|
||||
<p class="text-base-content/60 mb-4">No component categories found</p>
|
||||
<a href="{{ route('seller.business.settings.categories.create', [$business->slug, 'component']) }}"
|
||||
class="btn btn-sm">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Create First Category
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-right">Components</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($componentCategories as $category)
|
||||
<x-category-tree-item :category="$category" type="component" :business="$business" />
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-base-content/60">
|
||||
Total: {{ $componentCategories->count() }} top-level categories
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -2,27 +2,387 @@
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Company Information</p>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Company Information</h1>
|
||||
<div class="breadcrumbs hidden p-0 text-sm sm:inline">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li><a>Company</a></li>
|
||||
<li class="opacity-80">Company Information</li>
|
||||
<li><a href="{{ route('seller.business.settings.company-information', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">Company Information</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Company Information Settings</h2>
|
||||
<p class="text-base-content/60">Manage your company details, DBA, address, and other information.</p>
|
||||
<form action="{{ route('seller.business.settings.company-information.update', $business->slug) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Content (Left Side - 2 columns) -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Company Overview -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Company Overview</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Company Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Name <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="name" value="{{ old('name', $business->name) }}"
|
||||
class="input input-bordered @error('name') input-error @enderror"
|
||||
placeholder="Your company name" required>
|
||||
@error('name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- DBA Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">DBA Name</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="'Doing Business As' name if different from legal name">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" name="dba_name" value="{{ old('dba_name', $business->dba_name) }}"
|
||||
class="input input-bordered @error('dba_name') input-error @enderror"
|
||||
placeholder="Trade name or DBA">
|
||||
@error('dba_name')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Description (Full Width - Spans 2 columns) -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Description</span>
|
||||
<span class="label-text-alt flex items-center gap-2">
|
||||
<span class="tooltip tooltip-left" data-tip="This appears in the About Company modal">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
<span class="text-sm" id="description-count">0/500</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea name="description"
|
||||
rows="4"
|
||||
class="textarea textarea-bordered w-full resize-none @error('description') textarea-error @enderror"
|
||||
placeholder="Describe your company, mission, and what makes you unique"
|
||||
maxlength="500"
|
||||
oninput="updateCharCount('description', 'description-count', 500)">{{ old('description', $business->description) }}</textarea>
|
||||
@error('description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company Branding -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Company Branding</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Company Logo -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Logo</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Displayed in About Company modal">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
@if($business->logo_path && \Storage::disk('public')->exists($business->logo_path))
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $business->logo_path) }}" alt="Company logo" class="w-32 h-32 rounded-lg border border-base-300 object-contain bg-base-50 p-2">
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="logo" accept="image/*"
|
||||
class="file-input file-input-bordered @error('logo') file-input-error @enderror"
|
||||
onchange="previewImage(this, 'logo-preview')">
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Max 2MB. Recommended: Square (512x512px)</span>
|
||||
</label>
|
||||
@error('logo')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
<div id="logo-preview" class="mt-2 hidden">
|
||||
<p class="text-sm text-base-content/60 mb-1">Preview:</p>
|
||||
<img src="" alt="Logo preview" class="w-32 h-32 rounded-lg border border-base-300 object-contain bg-base-50 p-2">
|
||||
</div>
|
||||
@if($business->logo_path && \Storage::disk('public')->exists($business->logo_path))
|
||||
<label class="label cursor-pointer justify-start gap-2 mt-2">
|
||||
<input type="checkbox" name="remove_logo" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current logo</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Company Banner -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Company Banner</span>
|
||||
<span class="label-text-alt tooltip tooltip-left" data-tip="Displayed as hero banner on brand preview pages">
|
||||
<span class="icon-[lucide--info] size-4"></span>
|
||||
</span>
|
||||
</label>
|
||||
@if($business->banner_path && \Storage::disk('public')->exists($business->banner_path))
|
||||
<div class="mb-2">
|
||||
<img src="{{ asset('storage/' . $business->banner_path) }}" alt="Company banner" class="w-full h-24 rounded-lg border border-base-300 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="banner" accept="image/*"
|
||||
class="file-input file-input-bordered @error('banner') file-input-error @enderror"
|
||||
onchange="previewImage(this, 'banner-preview')">
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Max 4MB. Recommended: 1920x640px (3:1 ratio)</span>
|
||||
</label>
|
||||
@error('banner')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
<div id="banner-preview" class="mt-2 hidden">
|
||||
<p class="text-sm text-base-content/60 mb-1">Preview:</p>
|
||||
<img src="" alt="Banner preview" class="w-full h-24 rounded-lg border border-base-300 object-cover">
|
||||
</div>
|
||||
@if($business->banner_path && \Storage::disk('public')->exists($business->banner_path))
|
||||
<label class="label cursor-pointer justify-start gap-2 mt-2">
|
||||
<input type="checkbox" name="remove_banner" value="1" class="checkbox checkbox-sm">
|
||||
<span class="label-text text-sm">Remove current banner</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Contact Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Business Phone -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Business Phone</span>
|
||||
</label>
|
||||
<input type="tel" name="business_phone" value="{{ old('business_phone', $business->business_phone) }}"
|
||||
class="input input-bordered @error('business_phone') input-error @enderror"
|
||||
placeholder="(555) 123-4567">
|
||||
@error('business_phone')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Business Email -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Business Email</span>
|
||||
</label>
|
||||
<input type="email" name="business_email" value="{{ old('business_email', $business->business_email) }}"
|
||||
class="input input-bordered @error('business_email') input-error @enderror"
|
||||
placeholder="info@company.com">
|
||||
@error('business_email')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Physical Address -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">Physical Address</h2>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Street Address -->
|
||||
<div class="form-control col-span-12 md:col-span-8">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Street Address</span>
|
||||
</label>
|
||||
<input type="text" name="physical_address" value="{{ old('physical_address', $business->physical_address) }}"
|
||||
class="input input-bordered w-full @error('physical_address') input-error @enderror"
|
||||
placeholder="123 Main Street">
|
||||
@error('physical_address')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Suite/Unit Number -->
|
||||
<div class="form-control col-span-12 md:col-span-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Suite/Unit</span>
|
||||
</label>
|
||||
<input type="text" name="physical_suite" value="{{ old('physical_suite', $business->physical_suite) }}"
|
||||
class="input input-bordered w-full @error('physical_suite') input-error @enderror"
|
||||
placeholder="Suite 100">
|
||||
@error('physical_suite')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- City -->
|
||||
<div class="form-control col-span-12 md:col-span-6">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">City</span>
|
||||
</label>
|
||||
<input type="text" name="physical_city" value="{{ old('physical_city', $business->physical_city) }}"
|
||||
class="input input-bordered w-full @error('physical_city') input-error @enderror"
|
||||
placeholder="Phoenix">
|
||||
@error('physical_city')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- State -->
|
||||
<div class="form-control col-span-12 md:col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">State</span>
|
||||
</label>
|
||||
<input type="text" name="physical_state" value="{{ old('physical_state', $business->physical_state) }}"
|
||||
class="input input-bordered w-full @error('physical_state') input-error @enderror"
|
||||
placeholder="AZ"
|
||||
maxlength="2">
|
||||
@error('physical_state')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- ZIP Code -->
|
||||
<div class="form-control col-span-12 md:col-span-3">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">ZIP Code</span>
|
||||
</label>
|
||||
<input type="text" name="physical_zipcode" value="{{ old('physical_zipcode', $business->physical_zipcode) }}"
|
||||
class="input input-bordered w-full @error('physical_zipcode') input-error @enderror"
|
||||
placeholder="85001">
|
||||
@error('physical_zipcode')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar (1 column) -->
|
||||
<div class="lg:col-span-1 space-y-6">
|
||||
<!-- License Information -->
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="text-lg font-semibold mb-4">License Information</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- License Number -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">License Number</span>
|
||||
</label>
|
||||
<input type="text" name="license_number" value="{{ old('license_number', $business->license_number) }}"
|
||||
class="input input-bordered @error('license_number') input-error @enderror"
|
||||
placeholder="AZ-MED-00001234">
|
||||
@error('license_number')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- License Type -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">License Type</span>
|
||||
</label>
|
||||
<select name="license_type" class="select select-bordered @error('license_type') select-error @enderror">
|
||||
<option value="">Select license type</option>
|
||||
<option value="medical" {{ old('license_type', $business->license_type) == 'medical' ? 'selected' : '' }}>Medical</option>
|
||||
<option value="adult-use" {{ old('license_type', $business->license_type) == 'adult-use' ? 'selected' : '' }}>Adult Use</option>
|
||||
<option value="both" {{ old('license_type', $business->license_type) == 'both' ? 'selected' : '' }}>Both</option>
|
||||
</select>
|
||||
@error('license_type')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost gap-2">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2">
|
||||
<span class="icon-[lucide--save] size-4"></span>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Character counter
|
||||
function updateCharCount(textareaName, counterId, maxLength) {
|
||||
const textarea = document.querySelector(`[name="${textareaName}"]`);
|
||||
const counter = document.getElementById(counterId);
|
||||
if (textarea && counter) {
|
||||
counter.textContent = `${textarea.value.length}/${maxLength}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Image preview
|
||||
function previewImage(input, previewId) {
|
||||
const preview = document.getElementById(previewId);
|
||||
const img = preview.querySelector('img');
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
img.src = e.target.result;
|
||||
preview.classList.remove('hidden');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize character counters on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateCharCount('description', 'description-count', 500);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
302
resources/views/seller/settings/integrations.blade.php
Normal file
302
resources/views/seller/settings/integrations.blade.php
Normal file
@@ -0,0 +1,302 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-gradient-to-br from-slate-50 to-slate-100 py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header Section -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Integrations</h1>
|
||||
</div>
|
||||
<p class="text-gray-600 text-lg">Connect Cannabrands with your favorite apps to streamline your workflow</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="mb-8 flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search integrations..."
|
||||
class="input input-bordered w-full pl-10 bg-white shadow-sm"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<select class="select select-bordered bg-white shadow-sm w-full sm:w-48">
|
||||
<option disabled selected>Category</option>
|
||||
<option>All Categories</option>
|
||||
<option>Accounting</option>
|
||||
<option>Time Tracking</option>
|
||||
<option>Expense Management</option>
|
||||
<option>Financial Services</option>
|
||||
<option>Analytics</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Integrations Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
<!-- Integration Card 1: Dext Prepare -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Dext Prepare</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Automated receipt and invoice capture for seamless expense tracking</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.8</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(247 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 2: Bill.com -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Bill.com</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Streamline bill payments and receivables management</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.7</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(193 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 3: QuickBooks Time PREMIUM -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-bold text-gray-900">QuickBooks Time</h3>
|
||||
<span class="badge badge-sm badge-accent font-bold text-xs">PREMIUM</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Track employee hours and streamline payroll</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.6</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-gray-300 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(156 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 4: Fundbox -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Fundbox</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Fast business lines of credit when you need it</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.5</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(128 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 5: Expensify PREMIUM -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-indigo-400 to-indigo-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-bold text-gray-900">Expensify</h3>
|
||||
<span class="badge badge-sm badge-accent font-bold text-xs">PREMIUM</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Automate expense reporting and receipts management</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.9</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(312 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Card 6: Fathom -->
|
||||
<div class="card bg-white shadow-md hover:shadow-xl transition-shadow duration-300 border border-gray-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-start gap-3 flex-1">
|
||||
<div class="w-12 h-12 rounded-lg bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-gray-900">Fathom Analytics</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Get detailed insights into your business performance</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm font-semibold text-gray-900">4.4</span>
|
||||
<div class="flex gap-0.5 ml-1">
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-yellow-400 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
<svg class="w-4 h-4 text-gray-300 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">(89 reviews)</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Load More Button -->
|
||||
<div class="flex justify-center">
|
||||
<button class="btn btn-outline btn-lg gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Load More Integrations
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Smooth hover effects for cards */
|
||||
.card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* Star rating animation */
|
||||
svg.fill-current {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge-accent {
|
||||
background-color: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Smooth transitions on buttons */
|
||||
.btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Icon backgrounds with gradient */
|
||||
.bg-gradient-to-br {
|
||||
background-image: linear-gradient(135deg, var(--tw-gradient-stops));
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@@ -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
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Title and Breadcrumbs -->
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-medium">Orders</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">Orders</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Order Settings</h2>
|
||||
<p class="text-base-content/60">Configure order processing settings and preferences.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">This page is under construction.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@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
|
||||
125
resources/views/seller/wash-reports/index.blade.php
Normal file
125
resources/views/seller/wash-reports/index.blade.php
Normal file
@@ -0,0 +1,125 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid py-6">
|
||||
<!-- Page 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--flask-conical] size-8"></span>
|
||||
Wash Reports
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Track hash wash conversions and yields</p>
|
||||
</div>
|
||||
<a href="{{ route('seller.business.wash-reports.stage1', $business->slug) }}" class="btn btn-primary">
|
||||
<span class="icon-[lucide--plus] size-5"></span>
|
||||
New Wash Report
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Wash Reports Table -->
|
||||
<div class="card bg-base-100 shadow overflow-visible">
|
||||
<div class="card-body p-0 overflow-visible">
|
||||
<div class="flex items-center justify-between px-5 pt-5">
|
||||
<h2 class="font-bold text-lg">All Wash Reports</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-visible">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Report Name</th>
|
||||
<th>Strain</th>
|
||||
<th>Operator</th>
|
||||
<th>Wash Date</th>
|
||||
<th>Starting Weight</th>
|
||||
<th>Total Yield</th>
|
||||
<th>Yield %</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($conversions as $conversion)
|
||||
@php
|
||||
$stage1 = $conversion->metadata['stage_1'] ?? null;
|
||||
$stage2 = $conversion->metadata['stage_2'] ?? null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<a href="{{ route('seller.business.wash-reports.show', [$business->slug, $conversion]) }}" class="text-primary hover:underline">
|
||||
{{ $conversion->internal_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ $stage1['strain'] ?? 'N/A' }}</td>
|
||||
<td>{{ $conversion->operator?->name ?? 'N/A' }}</td>
|
||||
<td>{{ $stage1 ? \Carbon\Carbon::parse($stage1['wash_date'])->format('M j, Y') : 'N/A' }}</td>
|
||||
<td>{{ $stage1 ? number_format($stage1['starting_weight'], 1) . 'g' : 'N/A' }}</td>
|
||||
<td class="font-medium">{{ $stage2 ? number_format($stage2['total_yield'], 1) . 'g' : 'In Progress' }}</td>
|
||||
<td>
|
||||
@if($stage1 && $stage2 && $stage1['starting_weight'] > 0)
|
||||
<span class="font-semibold">{{ number_format(($stage2['total_yield'] / $stage1['starting_weight']) * 100, 1) }}%</span>
|
||||
@else
|
||||
<span class="text-base-content/40">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($conversion->status === 'completed')
|
||||
<span class="badge badge-sm badge-success">Completed</span>
|
||||
@elseif($conversion->status === 'in_progress')
|
||||
<span class="badge badge-sm badge-warning">In Progress</span>
|
||||
@else
|
||||
<span class="badge badge-sm badge-ghost">{{ ucfirst($conversion->status) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown {{ $loop->last ? 'dropdown-top' : '' }} dropdown-end">
|
||||
<button tabindex="0" class="btn btn-sm btn-ghost">
|
||||
<span class="icon-[lucide--more-vertical] size-4"></span>
|
||||
</button>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52 z-[100] {{ $loop->last ? 'mb-1' : '' }}">
|
||||
<li>
|
||||
<a href="{{ route('seller.business.wash-reports.show', [$business->slug, $conversion]) }}">
|
||||
<span class="icon-[lucide--eye] size-4"></span>
|
||||
View Details
|
||||
</a>
|
||||
</li>
|
||||
@if($conversion->status === 'in_progress' && !$stage2)
|
||||
<li>
|
||||
<a href="{{ route('seller.business.wash-reports.stage2', [$business->slug, $conversion]) }}">
|
||||
<span class="icon-[lucide--arrow-right] size-4"></span>
|
||||
Continue to Stage 2
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-8 text-base-content/60">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="icon-[lucide--flask-conical] size-12 text-base-content/40 mb-4"></span>
|
||||
<p class="font-medium">No wash reports yet</p>
|
||||
<p class="text-sm mt-1">Create your first wash report to track hash production</p>
|
||||
<a href="{{ route('seller.business.wash-reports.stage1', $business->slug) }}" class="btn btn-primary btn-sm mt-4">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
New Wash Report
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($conversions->hasPages())
|
||||
<div class="px-5 py-4">
|
||||
{{ $conversions->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
229
resources/views/seller/wash-reports/show.blade.php
Normal file
229
resources/views/seller/wash-reports/show.blade.php
Normal file
@@ -0,0 +1,229 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$stage1 = $conversion->metadata['stage_1'] ?? null;
|
||||
$stage2 = $conversion->metadata['stage_2'] ?? null;
|
||||
$yieldPercentage = ($stage1 && $stage2 && $stage1['starting_weight'] > 0)
|
||||
? ($stage2['total_yield'] / $stage1['starting_weight']) * 100
|
||||
: 0;
|
||||
@endphp
|
||||
|
||||
<div class="container-fluid py-6">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 text-sm breadcrumbs mb-2">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.wash-reports.index', $business->slug) }}">Wash Reports</a></li>
|
||||
<li>{{ $conversion->internal_name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content flex items-center gap-2">
|
||||
<span class="icon-[lucide--flask-conical] size-8"></span>
|
||||
{{ $conversion->internal_name }}
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Wash report details and yield data</p>
|
||||
</div>
|
||||
<div>
|
||||
@if($conversion->status === 'completed')
|
||||
<span class="badge badge-lg badge-success">Completed</span>
|
||||
@elseif($conversion->status === 'in_progress')
|
||||
<span class="badge badge-lg badge-warning">In Progress</span>
|
||||
@else
|
||||
<span class="badge badge-lg badge-ghost">{{ ucfirst($conversion->status) }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Stage 1: Wash Parameters -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--droplets] size-5"></span>
|
||||
Stage 1: Wash Parameters
|
||||
</h3>
|
||||
|
||||
@if($stage1)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Strain</p>
|
||||
<p class="font-semibold">{{ $stage1['strain'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Wash Date</p>
|
||||
<p class="font-semibold">{{ \Carbon\Carbon::parse($stage1['wash_date'])->format('M j, Y') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Cultivator</p>
|
||||
<p class="font-semibold">{{ $stage1['cultivator'] ?? 'N/A' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Starting Weight</p>
|
||||
<p class="font-semibold">{{ number_format($stage1['starting_weight'], 1) }}g</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Soak Time</p>
|
||||
<p class="font-semibold">{{ $stage1['soak_time_minutes'] }} minutes</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Room Temperature</p>
|
||||
<p class="font-semibold">{{ $stage1['room_temperature_f'] }}°F</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Vessel Temperature</p>
|
||||
<p class="font-semibold">{{ $stage1['vessel_temperature_f'] }}°F</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Operator</p>
|
||||
<p class="font-semibold">{{ $conversion->operator?->name ?? 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wash Cycles -->
|
||||
@if(isset($stage1['wash_cycles']) && count($stage1['wash_cycles']) > 0)
|
||||
<div class="divider my-4">Wash Cycles</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cycle</th>
|
||||
<th>Forward Speed</th>
|
||||
<th>Reverse Speed</th>
|
||||
<th>Pause (min)</th>
|
||||
<th>Run Time (min)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($stage1['wash_cycles'] as $cycle)
|
||||
<tr>
|
||||
<td>{{ $cycle['cycle'] }}</td>
|
||||
<td>{{ $cycle['forward_speed'] }}</td>
|
||||
<td>{{ $cycle['reverse_speed'] }}</td>
|
||||
<td>{{ $cycle['pause'] }}</td>
|
||||
<td>{{ $cycle['run_time'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="alert alert-warning">
|
||||
<span class="icon-[lucide--alert-triangle] size-5"></span>
|
||||
<span>Stage 1 data not available</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: Yield Data -->
|
||||
<div class="card bg-base-100 shadow-sm mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--package-check] size-5"></span>
|
||||
Stage 2: Yield Data
|
||||
</h3>
|
||||
|
||||
@if($stage2)
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
@foreach($stage2['yields'] as $yieldType => $yieldData)
|
||||
<div class="flex items-center justify-between p-3 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<p class="font-medium">{{ ucwords(str_replace('_', ' ', $yieldType)) }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold">{{ number_format($yieldData['weight'], 1) }}g</p>
|
||||
<p class="text-sm text-base-content/60">{{ number_format($yieldData['percentage'], 2) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-[lucide--info] size-5"></span>
|
||||
<div>
|
||||
<p class="font-medium">Stage 2 not completed</p>
|
||||
@if($conversion->status === 'in_progress')
|
||||
<p class="text-sm mt-1">
|
||||
<a href="{{ route('seller.business.wash-reports.stage2', [$business->slug, $conversion]) }}" class="link">
|
||||
Continue to Stage 2 to enter yield data
|
||||
</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: Summary Stats -->
|
||||
<div class="lg:col-span-1">
|
||||
<!-- Yield Summary -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4">Summary</h3>
|
||||
|
||||
@if($stage1 && $stage2)
|
||||
<div class="stats stats-vertical shadow w-full">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Starting Weight</div>
|
||||
<div class="stat-value text-2xl">{{ number_format($stage1['starting_weight'], 1) }}g</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Yield</div>
|
||||
<div class="stat-value text-2xl text-primary">{{ number_format($stage2['total_yield'], 1) }}g</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Yield Percentage</div>
|
||||
<div class="stat-value text-2xl text-success">{{ number_format($yieldPercentage, 1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-base-content/60">
|
||||
<span class="icon-[lucide--bar-chart] size-12 text-base-content/40 mb-2 block"></span>
|
||||
<p class="text-sm">Summary will be available after completing both stages</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
@if($conversion->notes)
|
||||
<div class="card bg-base-100 shadow-sm mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-2">Notes</h3>
|
||||
<p class="text-sm">{{ $conversion->notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Timestamps -->
|
||||
<div class="card bg-base-100 shadow-sm mt-6">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-3">Timeline</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base-content/60">Started:</span>
|
||||
<span class="font-medium">{{ $conversion->started_at?->format('M j, Y g:i A') ?? 'N/A' }}</span>
|
||||
</div>
|
||||
@if($conversion->completed_at)
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base-content/60">Completed:</span>
|
||||
<span class="font-medium">{{ $conversion->completed_at->format('M j, Y g:i A') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
257
resources/views/seller/wash-reports/stage1.blade.php
Normal file
257
resources/views/seller/wash-reports/stage1.blade.php
Normal file
@@ -0,0 +1,257 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 text-sm breadcrumbs mb-2">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li>Production</li>
|
||||
<li>New Wash Report</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<span class="icon-[lucide--beaker] size-8"></span>
|
||||
Hash Wash Report - Stage 1
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Enter wash parameters and cycles</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.wash-reports.stage1.store', $business->slug) }}" class="space-y-6 max-w-5xl" x-data="washReportForm()" novalidate>
|
||||
@csrf
|
||||
|
||||
<!-- Wash Parameters -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--settings] size-5"></span>
|
||||
Wash Parameters
|
||||
</h3>
|
||||
|
||||
<!-- Input Material Batch Selection -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Input Material Batch <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<div x-data="{
|
||||
init() {
|
||||
new Choices(this.$refs.batchSelect, {
|
||||
searchEnabled: true,
|
||||
searchPlaceholderValue: 'Type to search batches...',
|
||||
shouldSort: false,
|
||||
placeholder: true,
|
||||
placeholderValue: 'Select input material batch...',
|
||||
noResultsText: 'No batches found',
|
||||
noChoicesText: 'No batches available',
|
||||
allowHTML: false,
|
||||
itemSelectText: 'Press to select',
|
||||
})
|
||||
}
|
||||
}">
|
||||
<select x-ref="batchSelect" name="input_batch_id" class="select select-bordered w-full @error('input_batch_id') select-error @enderror" required x-model="selectedBatch">
|
||||
<option value="">Select input material batch...</option>
|
||||
@foreach($inputBatches as $batch)
|
||||
<option value="{{ $batch->id }}" {{ old('input_batch_id') == $batch->id ? 'selected' : '' }}>
|
||||
{{ $batch->batch_number }} - {{ $batch->strain ?? 'Unknown Strain' }} ({{ number_format($batch->quantity_remaining, 2) }}g available)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@error('input_batch_id')
|
||||
<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-4">
|
||||
<!-- Wash Date -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Wash Date <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="date" name="wash_date" class="input input-bordered @error('wash_date') input-error @enderror" value="{{ old('wash_date') }}" required />
|
||||
@error('wash_date')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Strain Name -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Strain / Varietal <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="text" name="strain" class="input input-bordered @error('strain') input-error @enderror" value="{{ old('strain') }}" placeholder="e.g., Banana Shack" required />
|
||||
@error('strain')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Starting Weight -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Starting Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="starting_weight" class="input input-bordered @error('starting_weight') input-error @enderror" value="{{ old('starting_weight') }}" placeholder="6000.00" required />
|
||||
@error('starting_weight')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Soak Time -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Soak Time (min) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" min="0" name="soak_time_minutes" class="input input-bordered @error('soak_time_minutes') input-error @enderror" value="{{ old('soak_time_minutes') }}" placeholder="15" required />
|
||||
@error('soak_time_minutes')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Room Temperature -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Room Temp (°F) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.1" name="room_temperature_f" class="input input-bordered @error('room_temperature_f') input-error @enderror" value="{{ old('room_temperature_f') }}" placeholder="58" required />
|
||||
@error('room_temperature_f')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Vessel Temperature -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Vessel Temp (°F) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.1" name="vessel_temperature_f" class="input input-bordered @error('vessel_temperature_f') input-error @enderror" value="{{ old('vessel_temperature_f') }}" placeholder="30" required />
|
||||
@error('vessel_temperature_f')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wash Cycles -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2">
|
||||
<span class="icon-[lucide--rotate-cw] size-5"></span>
|
||||
Wash Cycles
|
||||
</h3>
|
||||
<button type="button" @click="addCycle" class="btn btn-sm btn-primary">
|
||||
<span class="icon-[lucide--plus] size-4"></span>
|
||||
Add Cycle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/60 mb-4">Each cycle washes the same input material. Record different wash parameters for yield optimization tracking.</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">Cycle</th>
|
||||
<th class="text-center">Forward Speed</th>
|
||||
<th class="text-center">Reverse Speed</th>
|
||||
<th class="text-center">Pause (sec)</th>
|
||||
<th class="text-center">Run Time (min)</th>
|
||||
<th class="text-center w-12">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(cycle, index) in cycles" :key="cycle.id">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="hidden" :name="'wash_cycles[' + index + '][cycle]'" :value="index + 1" />
|
||||
<span class="font-medium" x-text="index + 1"></span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" :name="'wash_cycles[' + index + '][forward_speed]'" x-model="cycle.forward_speed" min="1" max="10" class="input input-bordered input-sm w-20 text-center" required />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" :name="'wash_cycles[' + index + '][reverse_speed]'" x-model="cycle.reverse_speed" min="1" max="10" class="input input-bordered input-sm w-20 text-center" required />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" :name="'wash_cycles[' + index + '][pause]'" x-model="cycle.pause" min="0" class="input input-bordered input-sm w-20 text-center" required />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" :name="'wash_cycles[' + index + '][run_time]'" x-model="cycle.run_time" min="1" class="input input-bordered input-sm w-20 text-center" required />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" @click="removeCycle(index)" class="btn btn-ghost btn-xs text-error" :disabled="cycles.length === 1">
|
||||
<span class="icon-[lucide--trash-2] size-4"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--sticky-note] size-5"></span>
|
||||
Additional Notes
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Optional - Add observations or special conditions</p>
|
||||
<div class="form-control">
|
||||
<textarea name="notes" rows="4" class="textarea textarea-bordered @error('notes') textarea-error @enderror" placeholder="e.g., Material quality observations, environmental conditions, equipment notes...">{{ old('notes') }}</textarea>
|
||||
@error('notes')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-[lucide--arrow-right] size-4"></span>
|
||||
Continue to Stage 2
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function washReportForm() {
|
||||
return {
|
||||
selectedBatch: '{{ old('input_batch_id') }}',
|
||||
cycles: {!! json_encode(old('wash_cycles', [])) !!},
|
||||
nextId: 1,
|
||||
|
||||
addCycle() {
|
||||
this.cycles.push({
|
||||
id: this.nextId++,
|
||||
cycle: this.cycles.length + 1,
|
||||
forward_speed: 7,
|
||||
reverse_speed: 7,
|
||||
pause: 5,
|
||||
run_time: 60
|
||||
});
|
||||
},
|
||||
|
||||
removeCycle(index) {
|
||||
if (this.cycles.length > 1) {
|
||||
this.cycles.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
242
resources/views/seller/wash-reports/stage2.blade.php
Normal file
242
resources/views/seller/wash-reports/stage2.blade.php
Normal file
@@ -0,0 +1,242 @@
|
||||
@extends('layouts.app-with-sidebar')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 text-sm breadcrumbs mb-2">
|
||||
<ul>
|
||||
<li><a href="{{ route('seller.business.dashboard', $business->slug) }}">Dashboard</a></li>
|
||||
<li>Production</li>
|
||||
<li>Hash Wash Report - Stage 2</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold flex items-center gap-2">
|
||||
<span class="icon-[lucide--beaker] size-8"></span>
|
||||
Hash Wash Report - Stage 2
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mt-1">Enter yield details by micron grade</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 max-w-5xl">
|
||||
<!-- Stage 1 Summary (Read-only) -->
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-4">
|
||||
<span class="icon-[lucide--check-circle] size-5 text-success"></span>
|
||||
Stage 1 Summary
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-base-content/60">Strain:</span>
|
||||
<p class="font-medium">{{ $stage1Data['strain'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">Starting Weight:</span>
|
||||
<p class="font-medium">{{ number_format($stage1Data['starting_weight'], 2) }}g</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">Wash Date:</span>
|
||||
<p class="font-medium">{{ \Carbon\Carbon::parse($stage1Data['wash_date'])->format('M d, Y') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">Wash Cycles:</span>
|
||||
<p class="font-medium">{{ count($stage1Data['wash_cycles']) }} cycles</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('seller.business.wash-reports.stage2.store', ['business' => $business->slug, 'conversion' => $conversion->id]) }}" class="space-y-6" x-data="yieldTrackingForm({{ $stage1Data['starting_weight'] }})" novalidate>
|
||||
@csrf
|
||||
|
||||
<!-- Yield Details by Grade -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2 mb-2">
|
||||
<span class="icon-[lucide--scale] size-5"></span>
|
||||
Hash Yield Details
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">Enter the weight collected for each micron grade. Percentages will be calculated automatically.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Fresh Press (120u) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center p-4 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<h4 class="font-semibold">Fresh Press (120u)</h4>
|
||||
<p class="text-sm text-base-content/60">Largest micron grade</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="fresh_press_120u" x-model.number="yields.fresh_press_120u" class="input input-bordered @error('fresh_press_120u') input-error @enderror" placeholder="0.00" required />
|
||||
@error('fresh_press_120u')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm text-base-content/60">Yield:</span>
|
||||
<p class="text-2xl font-bold" x-text="calculatePercentage(yields.fresh_press_120u) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cold Cure (90u) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center p-4 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<h4 class="font-semibold">Cold Cure (90u)</h4>
|
||||
<p class="text-sm text-base-content/60">Premium grade</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="cold_cure_90u" x-model.number="yields.cold_cure_90u" class="input input-bordered @error('cold_cure_90u') input-error @enderror" placeholder="0.00" required />
|
||||
@error('cold_cure_90u')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm text-base-content/60">Yield:</span>
|
||||
<p class="text-2xl font-bold text-primary" x-text="calculatePercentage(yields.cold_cure_90u) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rosin (45u) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center p-4 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<h4 class="font-semibold">Rosin (45u)</h4>
|
||||
<p class="text-sm text-base-content/60">Top quality</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="rosin_45u" x-model.number="yields.rosin_45u" class="input input-bordered @error('rosin_45u') input-error @enderror" placeholder="0.00" required />
|
||||
@error('rosin_45u')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm text-base-content/60">Yield:</span>
|
||||
<p class="text-2xl font-bold text-success" x-text="calculatePercentage(yields.rosin_45u) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Green/Blonde (160u) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center p-4 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<h4 class="font-semibold">Green/Blonde (160u)</h4>
|
||||
<p class="text-sm text-base-content/60">Larger micron</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="green_blonde_160u" x-model.number="yields.green_blonde_160u" class="input input-bordered @error('green_blonde_160u') input-error @enderror" placeholder="0.00" required />
|
||||
@error('green_blonde_160u')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm text-base-content/60">Yield:</span>
|
||||
<p class="text-2xl font-bold" x-text="calculatePercentage(yields.green_blonde_160u) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Green/Blonde (25u) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center p-4 bg-base-200 rounded-lg">
|
||||
<div>
|
||||
<h4 class="font-semibold">Green/Blonde (25u)</h4>
|
||||
<p class="text-sm text-base-content/60">Smallest micron</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Weight (g) <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" name="green_blonde_25u" x-model.number="yields.green_blonde_25u" class="input input-bordered @error('green_blonde_25u') input-error @enderror" placeholder="0.00" required />
|
||||
@error('green_blonde_25u')
|
||||
<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm text-base-content/60">Yield:</span>
|
||||
<p class="text-2xl font-bold" x-text="calculatePercentage(yields.green_blonde_25u) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Yield Summary -->
|
||||
<div class="divider my-6"></div>
|
||||
|
||||
<div class="bg-primary/10 p-6 rounded-lg">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="text-center">
|
||||
<span class="text-sm text-base-content/60">Starting Weight</span>
|
||||
<p class="text-3xl font-bold">{{ number_format($stage1Data['starting_weight'], 2) }}g</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="text-sm text-base-content/60">Total Yield</span>
|
||||
<p class="text-3xl font-bold text-primary" x-text="totalYield.toFixed(2) + 'g'"></p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="text-sm text-base-content/60">Overall Yield %</span>
|
||||
<p class="text-3xl font-bold text-success" x-text="totalPercentage.toFixed(2) + '%'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="btn btn-ghost">
|
||||
<span class="icon-[lucide--x] size-4"></span>
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-[lucide--check] size-4"></span>
|
||||
Complete Wash Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function yieldTrackingForm(startingWeight) {
|
||||
return {
|
||||
startingWeight: startingWeight,
|
||||
yields: {
|
||||
fresh_press_120u: {{ old('fresh_press_120u') ?: 0 }},
|
||||
cold_cure_90u: {{ old('cold_cure_90u') ?: 0 }},
|
||||
rosin_45u: {{ old('rosin_45u') ?: 0 }},
|
||||
green_blonde_160u: {{ old('green_blonde_160u') ?: 0 }},
|
||||
green_blonde_25u: {{ old('green_blonde_25u') ?: 0 }}
|
||||
},
|
||||
|
||||
get totalYield() {
|
||||
return this.yields.fresh_press_120u +
|
||||
this.yields.cold_cure_90u +
|
||||
this.yields.rosin_45u +
|
||||
this.yields.green_blonde_160u +
|
||||
this.yields.green_blonde_25u;
|
||||
},
|
||||
|
||||
get totalPercentage() {
|
||||
if (this.startingWeight <= 0) return 0;
|
||||
return (this.totalYield / this.startingWeight) * 100;
|
||||
},
|
||||
|
||||
calculatePercentage(weight) {
|
||||
if (this.startingWeight <= 0) return '0.00';
|
||||
return ((weight / this.startingWeight) * 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Storage Test</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="p-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">File Storage Test</h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<span class="text-sm">Current Disk: <strong>{{ config('filesystems.default') }}</strong></span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('storage.test') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Upload Test File</span>
|
||||
</label>
|
||||
<input type="file" name="test_file" class="file-input file-input-bordered" required>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button type="submit" class="btn btn-primary">Test Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divider">Storage Info</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-semibold">Disk</td>
|
||||
<td>{{ config('filesystems.default') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Driver</td>
|
||||
<td>{{ config('filesystems.disks.'.config('filesystems.default').'.driver') }}</td>
|
||||
</tr>
|
||||
@if(config('filesystems.default') === 's3')
|
||||
<tr>
|
||||
<td class="font-semibold">Endpoint</td>
|
||||
<td>{{ config('filesystems.disks.s3.endpoint') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Bucket</td>
|
||||
<td>{{ config('filesystems.disks.s3.bucket') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">URL</td>
|
||||
<td>{{ config('filesystems.disks.s3.url') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
@@ -114,6 +119,11 @@ Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
||||
|
||||
// Business-scoped routes requiring approval
|
||||
Route::prefix('{business}')->name('business.')->middleware(['auth', 'verified', 'approved'])->group(function () {
|
||||
// Redirect /s/{business} to /s/{business}/dashboard
|
||||
Route::get('/', function ($business) {
|
||||
return redirect()->route('seller.business.dashboard', $business);
|
||||
});
|
||||
|
||||
// Business Profile (read-only legal/compliance information)
|
||||
Route::get('/profile', [\App\Http\Controllers\Business\ProfileController::class, 'show'])->name('profile');
|
||||
|
||||
@@ -223,19 +233,88 @@ Route::prefix('s')->name('seller.')->middleware('seller')->group(function () {
|
||||
Route::delete('/{component}', [\App\Http\Controllers\Seller\ComponentController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Brand Management (business-scoped)
|
||||
Route::prefix('brands')->name('brands.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Seller\BrandController::class, 'index'])->name('index');
|
||||
Route::get('/create', [\App\Http\Controllers\Seller\BrandController::class, 'create'])->name('create');
|
||||
Route::post('/', [\App\Http\Controllers\Seller\BrandController::class, 'store'])->name('store');
|
||||
Route::get('/{brand}', [\App\Http\Controllers\Seller\BrandController::class, 'show'])->name('show');
|
||||
Route::get('/{brand}/preview', [\App\Http\Controllers\Seller\BrandController::class, 'preview'])->name('preview');
|
||||
Route::get('/{brand}/edit', [\App\Http\Controllers\Seller\BrandController::class, 'edit'])->name('edit');
|
||||
Route::put('/{brand}', [\App\Http\Controllers\Seller\BrandController::class, 'update'])->name('update');
|
||||
Route::delete('/{brand}', [\App\Http\Controllers\Seller\BrandController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Batch Management (manufacturing)
|
||||
Route::prefix('batches')->name('batches.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Seller\BatchController::class, 'index'])->name('index');
|
||||
Route::get('/create', [\App\Http\Controllers\Seller\BatchController::class, 'create'])->name('create');
|
||||
Route::post('/', [\App\Http\Controllers\Seller\BatchController::class, 'store'])->name('store');
|
||||
Route::get('/{batch}', [\App\Http\Controllers\Seller\BatchController::class, 'show'])->name('show');
|
||||
Route::get('/{batch}/edit', [\App\Http\Controllers\Seller\BatchController::class, 'edit'])->name('edit');
|
||||
Route::put('/{batch}', [\App\Http\Controllers\Seller\BatchController::class, 'update'])->name('update');
|
||||
Route::delete('/{batch}', [\App\Http\Controllers\Seller\BatchController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// Wash Report Management (compliance)
|
||||
Route::prefix('wash-reports')->name('wash-reports.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Seller\WashReportController::class, 'index'])->name('index');
|
||||
// Stage 1: Wash Parameters
|
||||
Route::get('/stage1', [\App\Http\Controllers\Seller\WashReportController::class, 'createStage1'])->name('stage1');
|
||||
Route::post('/stage1', [\App\Http\Controllers\Seller\WashReportController::class, 'storeStage1'])->name('stage1.store');
|
||||
// Stage 2: Yield Tracking
|
||||
Route::get('/stage2/{conversion}', [\App\Http\Controllers\Seller\WashReportController::class, 'createStage2'])->name('stage2');
|
||||
Route::post('/stage2/{conversion}', [\App\Http\Controllers\Seller\WashReportController::class, 'storeStage2'])->name('stage2.store');
|
||||
// Show individual wash report
|
||||
Route::get('/{conversion}', [\App\Http\Controllers\Seller\WashReportController::class, 'show'])->name('show');
|
||||
});
|
||||
|
||||
// View Switcher (business-scoped but outside settings group)
|
||||
Route::post('/view/switch', [\App\Http\Controllers\Seller\SettingsController::class, 'switchView'])->name('view.switch');
|
||||
|
||||
// Settings Management (business-scoped)
|
||||
Route::prefix('settings')->name('settings.')->group(function () {
|
||||
Route::get('/company-information', [\App\Http\Controllers\Seller\SettingsController::class, 'companyInformation'])->name('company-information');
|
||||
Route::put('/company-information', [\App\Http\Controllers\Seller\SettingsController::class, 'updateCompanyInformation'])->name('company-information.update');
|
||||
Route::get('/users', [\App\Http\Controllers\Seller\SettingsController::class, 'users'])->name('users');
|
||||
Route::get('/orders', [\App\Http\Controllers\Seller\SettingsController::class, 'orders'])->name('orders');
|
||||
Route::get('/brands', [\App\Http\Controllers\Seller\SettingsController::class, 'brands'])->name('brands');
|
||||
Route::get('/payments', [\App\Http\Controllers\Seller\SettingsController::class, 'payments'])->name('payments');
|
||||
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(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('/payments', [\App\Http\Controllers\Seller\SettingsController::class, 'payments'])->name('payments');
|
||||
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}', [\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}', [\App\Http\Controllers\Seller\SettingsController::class, 'updateUser'])->name('users.update');
|
||||
Route::delete('/users/{user}', [\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('/{type}/{id}/edit', [\App\Http\Controllers\Seller\CategoryController::class, 'edit'])->name('edit')->where('type', 'product|component');
|
||||
Route::put('/{type}/{id}', [\App\Http\Controllers\Seller\CategoryController::class, 'update'])->name('update')->where('type', 'product|component');
|
||||
Route::delete('/{type}/{id}', [\App\Http\Controllers\Seller\CategoryController::class, 'destroy'])->name('destroy')->where('type', 'product|component');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\StorageTestController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -199,9 +198,3 @@ Route::prefix('api')->group(function () {
|
||||
->name('api.check-email')
|
||||
->middleware('throttle:10,1'); // Rate limit: 10 requests per minute
|
||||
});
|
||||
|
||||
// Storage Test Routes (Development/Testing Only)
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/storage-test', [StorageTestController::class, 'form'])->name('storage.test.form');
|
||||
Route::post('/storage-test', [StorageTestController::class, 'test'])->name('storage.test');
|
||||
});
|
||||
|
||||
332
temp_orders_check.txt
Normal file
332
temp_orders_check.txt
Normal file
@@ -0,0 +1,332 @@
|
||||
@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">Order 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 href="{{ route('seller.business.settings.orders', $business->slug) }}">Settings</a></li>
|
||||
<li class="opacity-60">Orders</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('seller.business.settings.orders.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>
|
||||
|
||||
<!-- 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
|
||||
537
tests/Feature/ProductImageControllerTest.php
Normal file
537
tests/Feature/ProductImageControllerTest.php
Normal file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductImage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductImageControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
}
|
||||
|
||||
protected function withCsrfToken(): static
|
||||
{
|
||||
return $this->withSession(['_token' => 'test-token'])
|
||||
->withHeader('X-CSRF-TOKEN', 'test-token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can upload valid product image
|
||||
*/
|
||||
public function test_seller_can_upload_valid_product_image(): void
|
||||
{
|
||||
// Create seller business with brand and product
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create valid test image (750x384 minimum)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was created in database
|
||||
$this->assertDatabaseHas('product_images', [
|
||||
'product_id' => $product->id,
|
||||
'is_primary' => true, // First image should be primary
|
||||
]);
|
||||
|
||||
// Verify file was stored
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
Storage::disk('local')->assertExists($productImage->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test first uploaded image becomes primary
|
||||
*/
|
||||
public function test_first_image_becomes_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
$this->assertTrue($productImage->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test image upload validates minimum dimensions
|
||||
*/
|
||||
public function test_upload_validates_minimum_dimensions(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Image too small (below 750x384)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 500, 300);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test upload validates file type
|
||||
*/
|
||||
public function test_upload_validates_file_type(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Invalid file type
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $file]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot upload more than 6 images per product
|
||||
*/
|
||||
public function test_cannot_upload_more_than_six_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create 6 existing images
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => "products/test-{$i}.jpg",
|
||||
'is_primary' => $i === 0,
|
||||
'sort_order' => $i,
|
||||
]);
|
||||
}
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Maximum of 6 images allowed per product',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot upload image to another business's product (business_id isolation)
|
||||
*/
|
||||
public function test_seller_cannot_upload_image_to_other_business_product(): void
|
||||
{
|
||||
// Create two businesses
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to upload to businessB's product using businessA's slug
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$businessA->slug, $productB->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertNotFound(); // Product not found when scoped to businessA
|
||||
|
||||
// Verify no image was created
|
||||
$this->assertDatabaseMissing('product_images', [
|
||||
'product_id' => $productB->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product's image
|
||||
*/
|
||||
public function test_seller_can_delete_product_image(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create test file
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was deleted from database
|
||||
$this->assertDatabaseMissing('product_images', ['id' => $image->id]);
|
||||
|
||||
// Verify file was deleted from storage
|
||||
Storage::disk('local')->assertMissing('products/test.jpg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting primary image sets next image as primary
|
||||
*/
|
||||
public function test_deleting_primary_image_sets_next_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
Storage::disk('local')->put('products/test1.jpg', 'fake content 1');
|
||||
Storage::disk('local')->put('products/test2.jpg', 'fake content 2');
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Delete primary image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image1->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
// Verify image2 is now primary
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
$imageB = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to delete businessB's product image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$businessA->slug, $productB->id, $imageB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify image was NOT deleted
|
||||
$this->assertDatabaseHas('product_images', ['id' => $imageB->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can reorder product images
|
||||
*/
|
||||
public function test_seller_can_reorder_product_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create three images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$image3 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test3.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Reorder: image3, image1, image2
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$business->slug, $product->id]),
|
||||
['order' => [$image3->id, $image1->id, $image2->id]]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify new sort order
|
||||
$this->assertEquals(0, $image3->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(2, $image2->fresh()->sort_order);
|
||||
|
||||
// Verify first image (image3) is now primary
|
||||
$this->assertTrue($image3->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
$this->assertFalse($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot reorder another business's product images
|
||||
*/
|
||||
public function test_seller_cannot_reorder_other_business_product_images(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to reorder businessB's product images
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$businessA->slug, $productB->id]),
|
||||
['order' => [$image2->id, $image1->id]]
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify order was NOT changed
|
||||
$this->assertEquals(0, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image2->fresh()->sort_order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can set image as primary
|
||||
*/
|
||||
public function test_seller_can_set_image_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Set image2 as primary
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product->id, $image2->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image2 is now primary and image1 is not
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot set primary on another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_set_primary_on_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to set primary on businessB's product image
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$businessA->slug, $productB->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify is_primary was NOT changed
|
||||
$this->assertFalse($image->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot set primary on image that doesn't belong to product
|
||||
*/
|
||||
public function test_cannot_set_primary_on_image_from_different_product(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product1 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
$product2 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create image for product2
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product2->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Try to set it as primary for product1 (wrong product)
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product1->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertStatus(404);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Image not found',
|
||||
]);
|
||||
}
|
||||
}
|
||||
324
tests/Feature/ProductLineControllerTest.php
Normal file
324
tests/Feature/ProductLineControllerTest.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\ProductLine;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductLineControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test seller can create product line for their business
|
||||
*/
|
||||
public function test_seller_can_create_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line created successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name is required
|
||||
*/
|
||||
public function test_product_line_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name must be unique per business
|
||||
*/
|
||||
public function test_product_line_name_must_be_unique_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
// Create existing product line
|
||||
ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name can be duplicated across different businesses
|
||||
*/
|
||||
public function test_product_line_name_can_be_duplicated_across_businesses(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$sellerB = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerB->businesses()->attach($businessB->id);
|
||||
|
||||
// Create product line in business A
|
||||
ProductLine::create([
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
// Create product line with same name in business B (should work)
|
||||
$this->actingAs($sellerB);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success');
|
||||
|
||||
// Verify both exist
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can update their product line
|
||||
*/
|
||||
public function test_seller_can_update_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => 'Ultra Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line updated successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Ultra Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates name is required
|
||||
*/
|
||||
public function test_update_validates_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLine->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates uniqueness per business
|
||||
*/
|
||||
public function test_update_validates_uniqueness_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine1 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$productLine2 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Budget Line',
|
||||
]);
|
||||
|
||||
// Try to rename productLine2 to match productLine1
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine2->id]),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Budget Line', $productLine2->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot update another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_update_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$businessA->slug, $productLineB->id]),
|
||||
['name' => 'Hacked Name']
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLineB->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product line
|
||||
*/
|
||||
public function test_seller_can_delete_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$business->slug, $productLine->id])
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line deleted successfully.');
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$businessA->slug, $productLineB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify product line wasn't deleted
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLineB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller from unauthorized business cannot access another business routes
|
||||
*/
|
||||
public function test_seller_cannot_access_unauthorized_business_routes(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to access businessB routes
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Test Line']
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user