feat: implement import and export functionality for product categories and products
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\ProductCategories;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
|
||||
class ProductCategoriesExport implements FromCollection, WithHeadings, WithMapping
|
||||
{
|
||||
public function collection()
|
||||
{
|
||||
return ProductCategories::with('parent')->get();
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'ID',
|
||||
'Name',
|
||||
'Slug',
|
||||
'Parent Name',
|
||||
'Parent ID',
|
||||
'Type',
|
||||
'Is Active',
|
||||
'Sort Order',
|
||||
];
|
||||
}
|
||||
|
||||
public function map($category): array
|
||||
{
|
||||
return [
|
||||
$category->id,
|
||||
$category->name,
|
||||
$category->slug,
|
||||
$category->parent?->name,
|
||||
$category->parent_id,
|
||||
$category->type,
|
||||
$category->is_active ? 'Yes' : 'No',
|
||||
$category->sort_order,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Product;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
|
||||
class ProductsExport implements FromCollection, WithHeadings, WithMapping
|
||||
{
|
||||
public function collection()
|
||||
{
|
||||
return Product::with('category', 'pricings')->get();
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'ID',
|
||||
'Name',
|
||||
'Slug',
|
||||
'Category',
|
||||
'Category ID',
|
||||
'Short Description',
|
||||
'Description',
|
||||
'Setup Fee',
|
||||
'Trial Days',
|
||||
'CPU',
|
||||
'RAM',
|
||||
'GPU',
|
||||
'Disk',
|
||||
'Bandwidth',
|
||||
'Stock',
|
||||
'Is Unlimited',
|
||||
'Is Active',
|
||||
'Sort Order',
|
||||
'Product Type',
|
||||
'Published At',
|
||||
'Monthly Price',
|
||||
'Sale Price',
|
||||
];
|
||||
}
|
||||
|
||||
public function map($product): array
|
||||
{
|
||||
return [
|
||||
$product->id,
|
||||
$product->name,
|
||||
$product->slug,
|
||||
$product->category?->name,
|
||||
$product->product_categories_id,
|
||||
$product->short_description,
|
||||
$product->description,
|
||||
$product->setup_fee,
|
||||
$product->trial_days,
|
||||
$product->cpu,
|
||||
$product->ram,
|
||||
$product->gpu,
|
||||
$product->disk,
|
||||
$product->bandwidth,
|
||||
$product->stock,
|
||||
$product->is_unlimited ? 'Yes' : 'No',
|
||||
$product->is_active ? 'Yes' : 'No',
|
||||
$product->sort_order,
|
||||
$product->product_type,
|
||||
$product->published_at?->format('Y-m-d'),
|
||||
$product->pricings->where('billing_cycle', 'monthly')->where('is_active', 1)->first()?->price ?? '',
|
||||
$product->pricings->where('billing_cycle', 'monthly')->where('is_active', 1)->first()?->price_sale ?? '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProductCategories\ProductCategoryRequest;
|
||||
use App\Http\Services\Admin\ProductCategory\ProductCategoryServiceInterface;
|
||||
use App\Repositories\Admin\ProductCategory\ProductCategoryRepositoryInterface;
|
||||
use App\Exports\ProductCategoriesExport;
|
||||
use App\Imports\ProductCategoriesImport;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
|
||||
@@ -139,4 +142,33 @@ class ProductCategoryController extends Controller
|
||||
->rawColumns(['details_control', 'action', 'is_active', 'checkbox', 'sub_group_html'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
if ($request->get('sample') == 1) {
|
||||
return Excel::download(new ProductCategoriesExport(), 'product_categories_sample.xlsx');
|
||||
}
|
||||
return Excel::download(new ProductCategoriesExport(), 'product_categories_' . date('Y-m-d') . '.xlsx');
|
||||
}
|
||||
|
||||
public function import(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:xlsx,xls,csv'
|
||||
]);
|
||||
|
||||
try {
|
||||
Excel::import(new ProductCategoriesImport(), $request->file('file'));
|
||||
return redirect()->route('products.group.index')->with('success', 'Import product categories successfully');
|
||||
} catch (\Maatwebsite\Excel\Validators\ValidationException $e) {
|
||||
$failures = $e->failures();
|
||||
$errorMessage = 'Import failed at row(s): ';
|
||||
foreach ($failures as $failure) {
|
||||
$errorMessage .= $failure->row() . ' (' . implode(', ', $failure->errors()) . '); ';
|
||||
}
|
||||
return back()->withErrors($errorMessage);
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors('Import failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
namespace App\Http\Controllers\admin\product;
|
||||
|
||||
use App\Exports\ProductsExport;
|
||||
use App\Imports\ProductsImport;
|
||||
use App\Enums\Product\ProductTypeEnums;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Product\ProductRequest;
|
||||
use App\Http\Services\ProductService;
|
||||
use App\Repositories\Group\GroupRepositoryInterface;
|
||||
use App\Repositories\Admin\Product\ProductRepositoryInterface;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Exception;
|
||||
|
||||
@@ -222,4 +224,33 @@ class ProductController extends Controller
|
||||
default => 0
|
||||
};
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
if ($request->get('sample') == 1) {
|
||||
return Excel::download(new ProductsExport(), 'products_sample.xlsx');
|
||||
}
|
||||
return Excel::download(new ProductsExport(), 'products_' . date('Y-m-d') . '.xlsx');
|
||||
}
|
||||
|
||||
public function import(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:xlsx,xls,csv'
|
||||
]);
|
||||
|
||||
try {
|
||||
Excel::import(new ProductsImport(), $request->file('file'));
|
||||
return redirect()->route('products.index')->with('success', 'Import products successfully');
|
||||
} catch (\Maatwebsite\Excel\Validators\ValidationException $e) {
|
||||
$failures = $e->failures();
|
||||
$errorMessage = 'Import failed at row(s): ';
|
||||
foreach ($failures as $failure) {
|
||||
$errorMessage .= $failure->row() . ' (' . implode(', ', $failure->errors()) . '); ';
|
||||
}
|
||||
return back()->withErrors($errorMessage);
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors('Import failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\ProductCategories;
|
||||
use Illuminate\Support\Str;
|
||||
use Maatwebsite\Excel\Concerns\ToModel;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
|
||||
class ProductCategoriesImport implements ToModel, WithHeadingRow, WithValidation
|
||||
{
|
||||
public function model(array $row)
|
||||
{
|
||||
$parentId = null;
|
||||
|
||||
if (!empty($row['parent_name'])) {
|
||||
$parent = ProductCategories::where('name', $row['parent_name'])->first();
|
||||
$parentId = $parent?->id;
|
||||
}
|
||||
|
||||
if (!empty($row['parent_id'])) {
|
||||
$parentId = (int) $row['parent_id'];
|
||||
}
|
||||
|
||||
return new ProductCategories([
|
||||
'name' => $row['name'],
|
||||
'slug' => $row['slug'] ?? Str::slug($row['name']),
|
||||
'parent_id' => $parentId,
|
||||
'type' => $row['type'] ?? null,
|
||||
'is_active' => in_array(strtolower($row['is_active'] ?? ''), ['yes', '1', 'true']),
|
||||
'sort_order' => $row['sort_order'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'slug' => 'nullable|string|max:255',
|
||||
'parent_name' => 'nullable|string|max:255',
|
||||
'parent_id' => 'nullable|integer|exists:product_categories,id',
|
||||
'type' => 'nullable|string|max:50',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategories;
|
||||
use Illuminate\Support\Str;
|
||||
use Maatwebsite\Excel\Concerns\ToModel;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
|
||||
class ProductsImport implements ToModel, WithHeadingRow, WithValidation
|
||||
{
|
||||
public function model(array $row)
|
||||
{
|
||||
$categoryName = $row['category'] ?? null;
|
||||
$categoryId = null;
|
||||
|
||||
if ($categoryName) {
|
||||
$category = ProductCategories::where('name', $categoryName)->first();
|
||||
$categoryId = $category?->id;
|
||||
}
|
||||
|
||||
if (!empty($row['category_id'])) {
|
||||
$categoryId = (int) $row['category_id'];
|
||||
}
|
||||
|
||||
return new Product([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => $row['name'],
|
||||
'slug' => $row['slug'] ?? Str::slug($row['name']),
|
||||
'product_categories_id' => $categoryId,
|
||||
'short_description' => $row['short_description'] ?? null,
|
||||
'description' => $row['description'] ?? null,
|
||||
'setup_fee' => $row['setup_fee'] ?? 0,
|
||||
'trial_days' => $row['trial_days'] ?? 0,
|
||||
'cpu' => $row['cpu'] ?? null,
|
||||
'ram' => $row['ram'] ?? null,
|
||||
'gpu' => $row['gpu'] ?? null,
|
||||
'disk' => $row['disk'] ?? null,
|
||||
'bandwidth' => $row['bandwidth'] ?? null,
|
||||
'stock' => $row['stock'] ?? 0,
|
||||
'is_unlimited' => in_array(strtolower($row['is_unlimited'] ?? ''), ['yes', '1', 'true']),
|
||||
'is_active' => in_array(strtolower($row['is_active'] ?? ''), ['yes', '1', 'true']),
|
||||
'sort_order' => $row['sort_order'] ?? 0,
|
||||
'published_at' => !empty($row['published_at']) ? \Carbon\Carbon::parse($row['published_at']) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'slug' => 'nullable|string|max:255',
|
||||
'category' => 'nullable|string|max:255',
|
||||
'category_id' => 'nullable|integer|exists:product_categories,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,36 @@
|
||||
<x-button-crm type="button" variant="outline-info" size="sm" route="products.group.create" icon="plus-circle">Thêm
|
||||
nhóm
|
||||
SP</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-success" size="sm" route="products.export" icon="download">Export</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-warning" size="sm" onclick="$('#importProductModal').modal('show')" icon="upload">Import</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-danger" size="sm" route="products.group.trash" icon="trash">Thùng
|
||||
rác @if($trashedCount > 0)<span class="badge bg-danger ms-1">{{ $trashedCount }}</span>@endif</x-button-crm>
|
||||
@endif
|
||||
|
||||
@push('modals')
|
||||
<div class="modal fade" id="importProductModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Import Products</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ route('products.import') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="importFile" class="form-label">Choose Excel File</label>
|
||||
<input type="file" class="form-control" id="importFile" name="file" accept=".xlsx,.xls,.csv" required>
|
||||
<div class="form-text">Allowed files: .xlsx, .xls, .csv</div>
|
||||
</div>
|
||||
<a href="{{ route('products.export') }}?sample=1" class="btn btn-link btn-sm">Download Sample</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
@endif
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
@if(auth()->guard('admins')->user()->can("manager product"))
|
||||
<x-button-crm type="button" variant="outline-primary" size="sm" route="products.group.create" icon="plus-circle">Thêm
|
||||
nhóm SP</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-success" size="sm" route="products.group.export" icon="download">Export</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-warning" size="sm" onclick="$('#importCategoryModal').modal('show')" icon="upload">Import</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-danger" size="sm" route="products.group.trash" icon="trash">Thùng rác</x-button-crm>
|
||||
|
||||
@push('modals')
|
||||
<div class="modal fade" id="importCategoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Import Product Categories</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ route('products.group.import') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="importCategoryFile" class="form-label">Choose Excel File</label>
|
||||
<input type="file" class="form-control" id="importCategoryFile" name="file" accept=".xlsx,.xls,.csv" required>
|
||||
<div class="form-text">Allowed files: .xlsx, .xls, .csv</div>
|
||||
</div>
|
||||
<a href="{{ route('products.group.export') }}?sample=1" class="btn btn-link btn-sm">Download Sample</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
@endif
|
||||
@@ -36,7 +36,7 @@
|
||||
@if (auth()->guard('admins')->user()->can('manager product'))
|
||||
<div class="card rounded-4 shadow-none">
|
||||
<div class="card-header d-flex justify-content-end gap-2 rounded-top-4">
|
||||
@include('administrator.pages.block.product.btn')
|
||||
@include('administrator.pages.block.product.btn_categories')
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<x-datatable id="productTable" ajax="{{ route('products.group.datatable') }}"
|
||||
|
||||
@@ -63,4 +63,6 @@
|
||||
@foreach (config('assets.datatable.js') as $datatable)
|
||||
<script src="{{ asset($datatable) }}"></script>
|
||||
@endforeach
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@stack('modals')
|
||||
@@ -147,6 +147,8 @@ Route::prefix('')->group(function () {
|
||||
Route::delete('remove-product-item/{id}', 'destroy')->name('destroy');
|
||||
Route::post('duplicate-product/{id}', 'duplicate')->name('duplicate');
|
||||
Route::get('datatable', 'datatable')->name('datatable');
|
||||
Route::get('export', 'export')->name('export');
|
||||
Route::post('import', 'import')->name('import');
|
||||
|
||||
Route::controller(ProductCategoryController::class)->prefix('group')->as('group.')->group(function () {
|
||||
Route::get('all-group', 'index')->name('index');
|
||||
@@ -160,6 +162,8 @@ Route::prefix('')->group(function () {
|
||||
Route::get('trash', 'trash')->name('trash');
|
||||
Route::get('restore/{id}', 'restore')->name('restore');
|
||||
Route::delete('force-delete/{id}', 'forceDelete')->name('forceDelete');
|
||||
Route::get('export', 'export')->name('export');
|
||||
Route::post('import', 'import')->name('import');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user