tradings section

This commit is contained in:
saparatayev 2022-07-01 16:57:09 +05:00
parent 591fcbee2b
commit 325740f054
21 changed files with 922 additions and 5 deletions

View File

@ -43,7 +43,7 @@ class GroupController extends Controller
'title.tm' => ['required', 'min:3'],
'title.ru' => ['required', 'min:3'],
'title.en' => ['required', 'min:3'],
'type' => ['string', 'in:import,export'],
'type' => ['string', 'in:import,export,trading'],
'is_default' => ['boolean'],
]);
@ -74,7 +74,7 @@ class GroupController extends Controller
'title.ru' => ['required', 'min:3'],
'title.en' => ['required', 'min:3'],
'is_default' => ['boolean'],
'type' => ['string', 'in:import,export'],
'type' => ['string', 'in:import,export,trading'],
]);
info($request->all());

View File

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Web;
use Inertia\Inertia;
use App\Models\Group;
use App\Http\Controllers\Controller;
use Maatwebsite\Excel\Facades\Excel;
use App\Http\Resources\SubgroupResource;
use App\Imports\TradingsImport;
use App\Models\Subgroup;
class TradingController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
if (blank(request('group'))) {
request()->merge([
'group' => optional(Group::whereType('trading')->where('is_default', true)->first())->id
]);
}
$subgroupsWithTradings = Subgroup::with(['tradings' => function ($query) {
$query
->where('group_id', request('group'))
->where('locale', app()->getLocale());
}])
->where('group_id', request('group'))
->where('locale', app()->getLocale())
->simplePaginate(50);
$groups = Group::whereType('trading')->get();
$filters = array_filter(request()->all([
'group',
]));
if (array_key_exists('category', $filters)) {
$filters['category'] = intval($filters['category']);
}
if (array_key_exists('group', $filters)) {
$filters['group'] = intval($filters['group']);
}
return Inertia::render('Tradings', [
'text' => settings('text')[app()->getLocale()],
'filters' => $filters,
'subgroupsWithTradings' => SubgroupResource::collection($subgroupsWithTradings),
'groups' => fn () => $groups,
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function import()
{
request()->validate([
'group' => ['exists:groups,id'],
'file' => ['required', 'mimes:xlsx'],
]);
if (!$group = Group::find(request('group'))) {
$group = Group::create([
'title' => 'New group',
'type' => 'export',
'is_default' => true
]);
}
$group->tradings()->whereLocale(app()->getLocale())->delete();
try {
$id = now()->unix();
session(['import' => $id]);
$file = request()->file('file')->storeAs('uploads', $filename = $group->filename);
$group->update(['file' => $filename]);
Excel::queueImport(new TradingsImport($id, $group, app()->getLocale()), $file);
} catch (\Throwable $th) {
info('error here');
info($th->getMessage());
}
return redirect()->route('tradings');
}
public function status()
{
$id = session('import');
return response([
'started' => filled(cache("start_date_$id")),
'finished' => filled(cache("end_date_$id")),
'current_row' => (int) cache("current_row_$id"),
'total_rows' => (int) cache("total_rows_$id"),
]);
}
}

View File

@ -14,5 +14,6 @@ class VerifyCsrfToken extends Middleware
protected $except = [
'/exports/import',
'/imports/import',
'/tradings/import',
];
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class SubgroupResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'deals_count' => $this->deals_count,
'total_sum' => $this->total_sum,
'tradings' => TradingResource::collection($this->tradings)
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class TradingResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,206 @@
<?php
namespace App\Imports;
use App\Models\Group;
use App\Models\Export;
use App\Models\Category;
use App\Models\Subgroup;
use App\Models\Trading;
use Maatwebsite\Excel\Row;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Concerns\WithEvents;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class TradingsImport implements OnEachRow, WithStartRow, WithMultipleSheets, WithEvents, WithChunkReading, ShouldQueue
{
public $id;
public $group;
public $subgroup;
public $locale;
public $category;
public $currency;
public $categories;
public $tradingsInserts;
public function __construct(int $id, Group $group, string $locale)
{
$this->id = $id;
$this->group = $group->id;
$this->locale = $locale;
$this->categories = Category::all();
$this->tradingsInserts = [];
}
public function sheets(): array
{
return [
0 => $this,
];
}
public function startRow(): int
{
return 3;
}
public function chunkSize(): int
{
return 1000;
}
public function registerEvents(): array
{
return [
BeforeImport::class => function (BeforeImport $event) {
$totalRows = $event->getReader()->getTotalRows();
if (filled($totalRows)) {
cache()->forever("total_rows_{$this->id}", array_values($totalRows)[0]);
cache()->forever("start_date_{$this->id}", now()->unix());
}
},
AfterImport::class => function (AfterImport $event) {
cache(["end_date_{$this->id}" => now()], now()->addMinute());
cache()->forget("total_rows_{$this->id}");
cache()->forget("start_date_{$this->id}");
cache()->forget("current_row_{$this->id}");
},
];
}
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
$row = array_map('trim', $row->toArray());
cache()->forever("current_row_{$this->id}", $rowIndex);
if (!empty($row[0])) {
$this->setCategory($row);
$this->setCurrency($row);
$this->setType($row);
$this->setTradingsInSubgroup($row);
return;
}
if(empty($row[0]) && empty($row[1]) && empty($row[3]) && !empty($row[4])) {
$this->setTotalSumInSubgroup($row);
return;
}
$row['group'] = $this->group;
$row['category'] = $this->category;
$row['currency'] = $this->currency;
$row['locale'] = $this->locale;
$row['type'] = $this->type;
/**
* At first tradings are saved in an array, then in DB when subgroup ends
*/
array_push($this->tradingsInserts, [
'locale' => $row['locale'],
'category_id' => $row['category'],
'group_id' => $row['group'],
'subgroup_id' => null,
'type' => $row['type'],
'currency' => $row['currency'],
'title' => $row[1],
'unit' => $row[2],
'amount' => $row[3],
'price' => $row[4],
'seller_country' => $row[7],
'buyer_country' => $row[9],
'point' => $row[10],
]);
}
protected function setTotalSumInSubgroup($row)
{
if(strripos($row[4], 'Итого сумма') !== false || strripos($row[4], 'Total sum') !== false || strripos($row[4], 'Jemi') !== false) {
$this->subgroup->update([
'total_sum' => $row[4] . ' ' . $row[5]
]);
}
}
/**
* Save all rows with tradings for one subgroup.
* The function works, when excel parser reaches the row with `Заключено сделок`
*/
protected function setTradingsInSubgroup($row)
{
if(strripos($row[0], 'Заключено сделок') !== false || strripos($row[0], 'Deals count') !== false || strripos($row[0], 'Tm translation') !== false) {
$this->subgroup = Subgroup::create([
'deals_count' => $row[0],
'group_id' => $this->group,
'locale' => $this->locale
]);
foreach ($this->tradingsInserts as &$item) {
$item['subgroup_id'] = $this->subgroup->id;
}
$this->subgroup->tradings()->createMany($this->tradingsInserts);
$this->tradingsInserts = [];
}
}
protected function setCategory($row)
{
if ($category = $this->categories->first(fn ($c) => data_get($c->getOriginal('title'), $this->locale) == $row[0])) {
$this->category = $category->id;
}
}
protected function setCurrency($row)
{
if (in_array($row[0], [
'Доллар США',
'ABŞ-nyň dollary',
'US dollar',
'in US dollars',
])) {
$this->currency = 'USD';
}
if (in_array($row[0], [
'türkmen manady',
'Туркменский манат',
'turkmen manats',
])) {
$this->currency = 'TMT';
}
}
protected function setType($row)
{
if (in_array($row[0], [
'External',
'Foreign',
'Внешний',
'Daşarky',
])) {
$this->type = 'external';
}
if (in_array($row[0], [
'Internal',
'internal',
'Внутренний',
'Içerki'
])) {
$this->type = 'internal';
}
}
}

View File

@ -25,4 +25,9 @@ class Category extends Model
{
return $this->hasMany(Export::class);
}
public function tradings()
{
return $this->hasMany(Trading::class);
}
}

View File

@ -29,6 +29,11 @@ class Group extends Model
return $this->hasMany(Export::class);
}
public function tradings()
{
return $this->hasMany(Trading::class);
}
public function imports()
{
return $this->hasMany(Import::class);

23
app/Models/Subgroup.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subgroup extends Model
{
use HasFactory;
protected $guarded = ['id'];
public function tradings()
{
return $this->hasMany(Trading::class);
}
public function group()
{
return $this->belongsTo(Group::class);
}
}

36
app/Models/Trading.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Trading extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $casts = [
'price' => 'float',
'total' => 'float',
'amount' => 'float',
];
protected $appends = ['total'];
protected $with = ['category'];
public function scopeLines($query)
{
return $query->where('is_line', true);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function getTotalAttribute()
{
return round($this->price * $this->amount, 2);
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ChangeTypeColumnInGroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('groups', function (Blueprint $table) {
DB::statement("ALTER TABLE `groups` CHANGE `type` `type` ENUM('import', 'export', 'trading') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'import';");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('groups', function (Blueprint $table) {
DB::statement("ALTER TABLE `groups` CHANGE `type` `type` ENUM('import', 'export') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'import';");
});
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSubgroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('subgroups', function (Blueprint $table) {
$table->id();
$table->string('deals_count')->nullable();
$table->string('total_sum')->nullable();
$table->unsignedBigInteger('group_id');
$table->foreign('group_id')->references('id')->on('groups');
$table->enum('locale', ['en', 'ru', 'tm'])
->default('tm')
->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('subgroups');
}
}

View File

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTradingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tradings', function (Blueprint $table) {
$table->id();
$table->foreignId('group_id')->nullable()->constrained()->onDelete('cascade');
$table->unsignedBigInteger('subgroup_id');
$table->foreign('subgroup_id')->references('id')->on('subgroups');
$table->foreignId('category_id')->nullable()->constrained()->onDelete('cascade');
$table->enum('type', ['internal', 'external'])->nullable();
$table->text('title');
$table->string('unit')->nullable();
$table->string('amount')->nullable();
$table->string('currency')->nullable();
$table->string('seller_country')->nullable();
$table->string('buyer_country')->nullable();
$table->string('point')->nullable();
$table->string('price')->nullable();
$table->boolean('is_line')->default(false);
$table->enum('locale', ['en', 'ru', 'tm'])
->default('tm')
->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tradings');
}
}

View File

@ -55,6 +55,10 @@ export default {
label: this.trans("Exports"),
path: "exports",
},
{
label: this.trans("Trading"),
path: "tradings",
},
{
label: this.trans("Requests"),
path: "requests",

View File

@ -12,6 +12,9 @@
<a-radio-button value="export">
{{ trans("Export") }}
</a-radio-button>
<a-radio-button value="trading">
{{ trans("Trading") }}
</a-radio-button>
</a-radio-group>
<a-button type="primary" @click="visible = true">{{
trans("Create")

View File

@ -0,0 +1,267 @@
<template>
<div>
<!-- Header -->
<div
class="flex bg-white p-4 text-lg border-b border-gray-300 h-20 items-center"
>
<div class="flex-1 flex justify-end items-center space-x-3">
<a-button
v-if="$page.props.user"
type="primary"
@click="importModalVisible = true"
>{{ trans('Upload excel') }}</a-button
>
<a-button
v-if="group && group.file"
type="primary"
@click="() => download(group.hashid)"
>{{ trans('Download excel') }}</a-button
>
<a-select
v-model="filter.group"
:placeholder="trans('Group')"
class="w-48"
show-search
>
<a-select-option
v-for="group in groups"
:key="group.id"
:value="group.id"
>{{ group.title[$page.props.locale || 'tm'] }}</a-select-option
>
</a-select>
</div>
</div>
<!-- Table -->
<a-table
v-for="(subgroup, index) in items"
:key="subgroup.id"
class="table w-full"
:dataSource="subgroup.tradings"
:columns="visibleColumns"
:pagination="false"
:showHeader="index === 0 ? true : false"
:customRow="
(record) => {
let self = this;
return {
on: {},
};
}
"
rowKey="id"
>
<span
slot="id"
slot-scope="_, __, index"
class="text-right"
v-text="index + 1"
/>
<span slot="category" slot-scope="category">
<span>{{ category && category.title[$page.props.locale || 'tm'] }}</span>
</span>
<span slot="price" slot-scope="price, row" class="whitespace-nowrap">
{{ price }} {{ row.currency }}
</span>
<span slot="amount" slot-scope="amount, row" class="whitespace-nowrap">
{{ amount }} {{ row.unit }}
</span>
<template #footer>
<p class="footer-p">{{ subgroup.deals_count }}, {{ subgroup.total_sum }}</p>
</template>
</a-table>
<infinite-loading
:identifier="infiniteScroll"
:distance="20"
spinner="waveDots"
@infinite="loadMore"
v-if="items.length >= perPage"
>
<div slot="no-more" class="uppercase font-bold text-gray-700 mb-6 py-5">
{{ trans('All items loaded') }}
</div>
<div slot="no-results"></div>
</infinite-loading>
<!-- Other -->
<import-modal
v-if="importModalVisible"
action="tradings"
:group="group && group.id"
@close="importModalVisible = false"
/>
</div>
</template>
<script>
import columns from "@/data/trading-columns";
import ImportModal from "@/Components/ImportModal";
import InfiniteLoading from "vue-infinite-loading";
export default {
props: [
"subgroupsWithTradings",
"groups",
"filters",
],
components: { ImportModal, InfiniteLoading },
metaInfo() {
return {
title: this.trans('Trading'),
}
},
data() {
this.loadMore = _.debounce(this.loadMore, 300);
const showColumns = localStorage.getItem("trading-columns")
? JSON.parse(localStorage.getItem("trading-columns"))
: columns.filter((col) => col.visible).map((col) => col.key);
return {
page: 1,
perPage: 50,
infiniteScroll: +new Date(),
importModalVisible: false,
createRequestVisible: false,
items: this.subgroupsWithTradings.data,
selectedRowKeys: [],
selectedItems: [],
showOnlySelected: false,
showColumns,
filter: _.assign(
{
title: undefined,
category: undefined,
group: undefined,
currency: undefined,
unit: undefined,
type: undefined,
},
this.filters
)
};
},
watch: {
filter: {
handler(value) {
this.$inertia.get(this.route("tradings"), value, {
preserveState: true,
onSuccess: (response) => {
this.page = 1;
this.infiniteScroll = +new Date();
this.items = response.props.subgroupsWithTradings.data;
},
});
},
deep: true,
},
showColumns(value) {
localStorage.setItem("trading-columns", JSON.stringify(value));
},
},
computed: {
itemsFiltered() {
return this.items.filter(
(item) => !this.selectedRowKeys.includes(item.id)
);
},
visibleColumns() {
return [
...this.columns.filter((col) => this.showColumns.includes(col.key)),
];
},
group() {
return this.groups.find((group) => group.id === this.filter.group)
},
columns() {
return columns.map((column) => {
column.title = this.trans(column.title)
return column
});
}
},
beforeMount() {
if (this.$page.url.search('page') !== -1) {
this.$inertia.get(this.route("tradings"), this.filter)
}
},
methods: {
onSelectChange(selectedRowKeys, selectedRows) {
const items = _.differenceBy(this.selectedItems, selectedRows, "id");
this.selectedItems = [...items, ...selectedRows].filter((item) =>
selectedRowKeys.includes(item.id)
);
this.selectedRowKeys = selectedRowKeys;
},
onCheckboxChange(value) {
this.showColumns = [...value];
},
addToLine() {
this.$inertia.post(
this.route("lines.store"),
{
ids: this.selectedRowKeys,
},
{
onSuccess: (response) => {
this.$message.success("Lines added");
},
}
);
},
loadMore($state) {
this.$inertia.get(
this.route("tradings"),
{
...this.filter,
page: this.page + 1,
per_page: this.perPage,
},
{
only: ["subgroupsWithTradings"],
preserveState: true,
preserveScroll: true,
onSuccess: (response) => {
if (response.props.subgroupsWithTradings.data.length) {
this.items = this.items.concat(response.props.subgroupsWithTradings.data);
this.page += 1;
$state.loaded();
} else {
$state.complete();
}
},
}
);
},
},
};
</script>
<style scoped>
.footer-p {
font-weight: bold;
}
</style>

View File

@ -0,0 +1,81 @@
[
{
"key": "id",
"title": "№",
"align": "center",
"visible": true,
"scopedSlots": {
"customRender": "id"
},
"width": "1%",
"opacity": 0
},
{
"dataIndex": "title",
"key": "title",
"title": "Title",
"visible": true,
"width": "30%"
},
{
"dataIndex": "category",
"key": "category",
"title": "Category",
"visible": true,
"scopedSlots": {
"customRender": "category"
},
"width": "9%"
},
{
"dataIndex": "amount",
"key": "amount",
"title": "Amount",
"scopedSlots": {
"customRender": "amount"
},
"visible": true,
"width": "9%"
},
{
"dataIndex": "price",
"key": "price",
"title": "Price",
"scopedSlots": {
"customRender": "price"
},
"visible": true,
"width": "9%"
},
{
"dataIndex": "total",
"key": "total",
"title": "Total",
"scopedSlots": {
"customRender": "price"
},
"visible": true,
"width": "9%"
},
{
"dataIndex": "seller_country",
"key": "seller_country",
"title": "Seller country",
"visible": true,
"width": "7%"
},
{
"dataIndex": "buyer_country",
"key": "buyer_country",
"title": "Buyer country",
"visible": true,
"width": "7%"
},
{
"dataIndex": "point",
"key": "point",
"title": "Point",
"visible": true,
"width": "14%"
}
]

View File

@ -69,5 +69,8 @@
"First name": "First name",
"Last name": "Last name",
"Organization type": "Organization type",
"Unauthorized": "Unauthorized"
"Unauthorized": "Unauthorized",
"Seller country": "Seller country",
"Buyer country": "Buyer country",
"Trading": "Trading"
}

View File

@ -69,5 +69,8 @@
"First name": "Имя",
"Last name": "Фамилия",
"Organization type": "Тип организации",
"Unauthorized": "Неверные данные"
"Unauthorized": "Неверные данные",
"Seller country": "Страна продавца",
"Buyer country": "Страна покупателя",
"Trading": "Торги"
}

View File

@ -69,5 +69,8 @@
"First name": "Adyňyz",
"Last name": "Familiýaňyz",
"Organization type": "Edaraň görnüşi",
"Unauthorized": "Nädogry maglumat"
"Unauthorized": "Nädogry maglumat",
"Seller country": "Satyjynyň yurdy",
"Buyer country": "Alyjynyň yurdy",
"Trading": "Söwda"
}

View File

@ -9,6 +9,7 @@ use App\Http\Controllers\Web\ImportController;
use App\Http\Controllers\Web\RequestController;
use App\Http\Controllers\Web\SettingController;
use App\Http\Controllers\Web\CategoryController;
use App\Http\Controllers\Web\TradingController;
use App\Models\Export;
/*
@ -25,6 +26,7 @@ use App\Models\Export;
// Route::get('/', [HomeController::class, 'index'])->name('home');
Route::group(['middleware' => 'check_october_session'], function () {
Route::get('imports', [ImportController::class, 'index'])->name('imports');
Route::get('tradings', [TradingController::class, 'index'])->name('tradings');
Route::get('/', [ExportController::class, 'index'])->name('exports');
Route::get('download/{group}', [GroupController::class, 'download'])->name('download');
Route::post('requests', [RequestController::class, 'store'])->name('requests.store');
@ -35,6 +37,7 @@ Route::group(['middleware' => 'check_october_session'], function () {
Route::group(['middleware' => 'auth:sanctum'], function () {
Route::post('imports/import', [ImportController::class, 'import'])->name('imports.import');
Route::post('exports/import', [ExportController::class, 'import'])->name('exports.import');
Route::post('tradings/import', [TradingController::class, 'import'])->name('tradings.import');
Route::get('import-status', [ExportController::class, 'status'])->name('import-status');
Route::get('lines', [LineController::class, 'index'])->name('lines');