feat: implement import and export functionality for product categories and products

This commit is contained in:
pbao9
2026-02-21 11:28:03 +07:00
parent bfabd54176
commit 71e70b01da
11 changed files with 359 additions and 4 deletions
+44
View File
@@ -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,
];
}
}
+72
View File
@@ -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());
}
}
}
+46
View File
@@ -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',
];
}
}
+59
View File
@@ -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')
+4
View File
@@ -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');
});
});