xController list + template-list

pull/44/head
A1Gard 6 months ago
parent 5a3deaa61b
commit 56110bb254

@ -7,7 +7,7 @@ use App\Helpers;
* @param $lang code like fa * @param $lang code like fa
* @return string * @return string
*/ */
function getEmojiLanguagebyCode($lang) function getEmojiLanguagebyCode($lang) : string
{ {
$languages = [ $languages = [
"af" => "🇿🇦", // Afrikaans "af" => "🇿🇦", // Afrikaans
@ -88,13 +88,14 @@ function getEmojiLanguagebyCode($lang)
/** /**
* has route as named we want this model? * has route as named we want this model?
* @param $name string * @param $name string
* @param $endRoute string 'index' or alt list
* @return bool * @return bool
*/ */
function hasRoute($name) function hasRoute($name,$endRoute = 'index') : bool
{ {
// create route // create route
$cRuote = str_replace('index', $name, request()->route()->getName()); $cRuote = str_replace($endRoute, $name, request()->route()->getName());
if (\Illuminate\Support\Facades\Route::has($cRuote)) { if (\Illuminate\Support\Facades\Route::has($cRuote)) {
return true; return true;
} else { } else {
@ -106,12 +107,13 @@ function hasRoute($name)
* get named route url * get named route url
* @param $name string * @param $name string
* @param $args array * @param $args array
* @param $endRoute string 'index' or alt list
* @return string|null * @return string|null
*/ */
function getRoute($name,$args = []) function getRoute($name, $args = [],$endRoute = 'index') : string | null
{ {
// create route // create route
$cRuote = str_replace('index', $name, request()->route()->getName()); $cRuote = str_replace($endRoute, $name, request()->route()->getName());
if (\Illuminate\Support\Facades\Route::has($cRuote)) { if (\Illuminate\Support\Facades\Route::has($cRuote)) {
return \route($cRuote, $args); return \route($cRuote, $args);
} else { } else {
@ -125,7 +127,8 @@ function getRoute($name,$args = [])
* @param $col string * @param $col string
* @return string * @return string
*/ */
function sortSuffix($col){ function sortSuffix($col) : string
{
if (request()->sort == $col) { if (request()->sort == $col) {
if (request('sortType', 'asc') == 'desc') { if (request('sortType', 'asc') == 'desc') {
return '&sortType=asc'; return '&sortType=asc';
@ -136,3 +139,67 @@ function sortSuffix($col){
return ''; return '';
} }
} }
/**
* make array compatible | help us to translate
* @param $array
* @param $translate
* @return false|string
*/
function arrayNormolizeVueCompatible($array, $translate = false): false | string
{
$result = [];
foreach ($array as $index => $item) {
$result[] = ['id' => $index, 'name' => ($translate ? __($item) : $item)];
}
return json_encode($result);
}
/**
* check string is json or not
* @param $string
* @return bool
*/
function isJson($string) : bool {
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* save admin batch log
* @param $method
* @param $cls class
* @param $ids
* @return void
*/
function logAdminBatch($method, $cls, $ids): void
{
$act = explode('\\', $method);
foreach ($ids as $id) {
auth()->user()->logs()->create([
'action' => $act[count($act) - 1],
'loggable_type' => $cls,
'loggable_id' => $id,
]);
}
}
/**
* save admin log
* @param $method
* @param $cls class
* @param $id
* @return void
*/
function logAdmin($method, $cls, $id) :void
{
$act = explode('\\', $method);
auth()->user()->logs()->create([
'action' => $act[count($act) - 1],
'loggable_type' => $cls,
'loggable_id' => $id,
]);
}

@ -2,8 +2,12 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\CreateOrUpdate;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\XController; use App\Http\Controllers\XController;
use App\Models\Item;
use App\Models\User;
use App\SafeController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Helper; use App\Helper;
use function App\Helpers\hasCreateRoute; use function App\Helpers\hasCreateRoute;
@ -11,4 +15,68 @@ use function App\Helpers\hasCreateRoute;
class UserController extends XController class UserController extends XController
{ {
protected $cols = ['name', 'email', 'role', 'mobile']; protected $cols = ['name', 'email', 'role', 'mobile'];
protected $filterables = ['role'];
protected $searchable = ['name','mobile','email'];
protected $buttons = [
'edit' =>
['title' => "Edit", 'class' => 'btn-outline-primary', 'icon' => 'ri-edit-2-line'],
'show' =>
['title' => "Detail", 'class' => 'btn-outline-light', 'icon' => 'ri-eye-line'],
'log' =>
['title' => "Logs", 'class' => 'btn-outline-light', 'icon' => 'ri-file-list-2-line'],
'destroy' =>
['title' => "Remove", 'class' => 'btn-outline-danger delete-confirm', 'icon' => 'ri-close-line'],
];
public function createOrUpdate( $item, $request)
{
}
public function bulk(Request $request){
// dd($request->all());
$data = explode('.', $request->input('action'));
$action = $data[0];
$ids = $request->input('id');
switch ($action) {
case 'delete':
$msg = __(':COUNT items deleted successfully',['COUNT' => count($ids)]);
$this->model::destroy($ids);
break;
case 'restore':
$msg = __(':COUNT items restored successfully',['COUNT' => count($ids)]);
foreach ($ids as $id) {
$this->model::withTrashed()->find($id)->restore();
}
break;
case 'role':
foreach ($ids as $id) {
$user = User::where('id',$id)->first();
$user->role = $data[1];
$user->syncRoles([strtolower($data[1])]);
$user->save();
}
$msg = __(':COUNT user role changed to :NEWROLE successfully',['COUNT' => count($ids), 'NEWROLE' => __($data[1])]);
break;
default:
$msg = __('Unknown bulk action : :ACTION', ["ACTION" => $action] );
}
return $this->do_bulk($msg,$action, $ids);
}
public function destroy(User $item)
{
return parent::delete($item);
}
public function restore($item)
{
return parent::restoreing(User::withTrashed()->where('email',$item)->first());
}
} }

@ -3,9 +3,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class XController extends Controller abstract class XController extends Controller
{ {
protected $model = User::class; protected $model = User::class;
@ -14,27 +15,72 @@ class XController extends Controller
protected $extra_cols = ['id']; protected $extra_cols = ['id'];
protected $listView = 'admin.users.user-list'; protected $listView = 'admin.users.user-list';
protected $formView = 'admin.users.user-form'; protected $formView = 'admin.users.user-form';
protected $filterables = [];
protected $searchable = [];
public function createOrUpdate($item, Request $request)
protected $buttons = [
'edit' =>
['title' => "Edit", 'class' => 'btn-outline-primary', 'icon' => 'ri-edit-2-line'],
'destroy' =>
['title' => "Remove", 'class' => 'btn-outline-danger delete-confirm', 'icon' => 'ri-edit-2-line'],
];
public function createOrUpdate($item, $request)
{ {
} }
protected function showList($query) protected function showList($query)
{ {
$items = $query->paginate(config('app.panel.page_count'), array_merge($this->extra_cols, $this->cols));
if (hasRoute('trashed')){
$this->extra_cols[] = 'deleted_at';
}
$items = $query->paginate(config('app.panel.page_count'),
array_merge($this->extra_cols, $this->cols));
$cols = $this->cols; $cols = $this->cols;
return view($this->listView, compact('items', 'cols')); $buttons = $this->buttons;
return view($this->listView, compact('items', 'cols', 'buttons'));
} }
protected function makeSortAndFilter() protected function makeSortAndFilter()
{ {
if (!\request()->has('sort') || !in_array(\request('sort'), $this->cols)) { if (!\request()->has('sort') || !in_array(\request('sort'), $this->cols)) {
$query = $this->model::orderByDesc('id'); $query = $this->model::orderByDesc('id');
} else { } else {
$query = $this->model::orderBy(\request('sort'), \request('sortType', 'asc')); $query = $this->model::orderBy(\request('sort'), \request('sortType', 'asc'));
} }
foreach (\request()->input('filter', []) as $col => $filter) {
if (isJson($filter)) {
$vals = json_decode($filter);
if (count($vals) != 0) {
$query->whereIn($col, $vals);
}
} else {
$query->where($col, $filter);
}
}
if (mb_strlen(trim(\request()->input('q', ''))) > 0) {
foreach ($this->searchable as $col) {
$query->where(function ($query) {
foreach ($this->searchable as $key => $col) {
if ($key === 0) {
$query->where($col, 'LIKE', '%' . \request()->input('q', '') . '%');
} else {
$query->orWhere($col, 'LIKE', '%' . \request()->input('q', '') . '%');
}
}
});
}
}
return $query; return $query;
} }
@ -64,6 +110,15 @@ class XController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
// //
$item = new $this->model();
$item = $this->create($item,$request);
logAdmin(__METHOD__, $this->model , $item->id);
if ($request->ajax()){
return ['OK' => true,];
}else{
return redirect()->route('admin.'.$this->model.'.index')->with(['message' => __('As you wished created successfully')]);
}
} }
/** /**
@ -93,9 +148,23 @@ class XController extends Controller
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
*/ */
public function destroy($user) public function delete($item)
{ {
// //
logAdmin(__METHOD__, $this->model , $item->id);
$item->delete();
return redirect()->back()->with(['message' => __('As you wished removed successfully')]);
}
/**
* restore removed the specified resource from storage.
*/
public function restoreing($item)
{
//
logAdmin(__METHOD__, $this->model , $item->id);
$item->restore();
return redirect()->back()->with(['message' => __('As you wished restored successfully')]);
} }
@ -104,7 +173,13 @@ class XController extends Controller
*/ */
public function trashed() public function trashed()
{ {
$query = User::onlyTrashed(); $query = $this->makeSortAndFilter()->onlyTrashed();
return $this->showList($query); return $this->showList($query);
} }
protected function do_bulk($msg,$action,$ids)
{
logAdminBatch(__METHOD__ . '.' . $action, $this->model, $ids);
return redirect()->back()->with(['message' => $msg]);
}
} }

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Model;
class AdminLog extends Model class AdminLog extends Model
{ {
protected $guarded = [];
public function user() public function user()
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);

@ -13,7 +13,7 @@ class User extends Authenticatable
{ {
use HasFactory, Notifiable, HasRoles, SoftDeletes; use HasFactory, Notifiable, HasRoles, SoftDeletes;
static $roles = ['DEVELOPER', 'ADMIN', 'USER']; static $roles = ['DEVELOPER', 'ADMIN', 'USER', 'SUSPENDED'];
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -26,6 +26,11 @@ class User extends Authenticatable
'password', 'password',
]; ];
public function getRouteKeyName()
{
return 'email';
}
/** /**
* The attributes that should be hidden for serialization. * The attributes that should be hidden for serialization.
* *
@ -49,7 +54,13 @@ class User extends Authenticatable
]; ];
} }
public function posts(){ public function posts()
{
return $this->hasMany(Post::class); return $this->hasMany(Post::class);
} }
public function logs()
{
return $this->hasMany(AdminLog::class, 'user_id', 'id');
}
} }

@ -32,7 +32,7 @@ class BladeServiceProvider extends ServiceProvider
\$from = (\$currentPage - 1) * \$perPage + 1; \$from = (\$currentPage - 1) * \$perPage + 1;
\$to = min(\$currentPage * \$perPage, \$total); \$to = min(\$currentPage * \$perPage, \$total);
echo \"(\$from | \$to | \$total) \"; ?>"; echo \"( \$from - \$to ) \"; ?>";
}); });
} }
} }

@ -0,0 +1,9 @@
<?php
namespace App;
interface SafeController
{
//
public function createOrUpdate();
}

@ -17,6 +17,7 @@ import { createApp } from 'vue';
import './panel/raw.js'; import './panel/raw.js';
import './panel/navbar.js'; import './panel/navbar.js';
import './panel/list-checkboxs.js'; import './panel/list-checkboxs.js';
import './panel/general-events.js';
/** /**
* Next, we will create a fresh Vue application instance. You may then begin * Next, we will create a fresh Vue application instance. You may then begin
@ -29,6 +30,32 @@ const app = createApp({});
import ExampleComponent from './components/ExampleComponent.vue'; import ExampleComponent from './components/ExampleComponent.vue';
app.component('example-component', ExampleComponent); app.component('example-component', ExampleComponent);
import VueJalaliCalendar from './components/vueJalaliCalendar.vue';
app.component('vue-jalali-calendar', VueJalaliCalendar);
import CurrencyInput from './components/CurrencyInput.vue';
app.component('currency-input', CurrencyInput);
import RemixIconPicker from './components/RemixIconPicker.vue';
app.component('remix-icon-picker', RemixIconPicker);
import FontAwesomeIconPicker from "./components/FontAwesomeIconPicker.vue";
app.component('awesome-icon-picker', FontAwesomeIconPicker);
import vueDateTimePicker from "./components/vueDateTimePicker.vue";
app.component('vue-datetime-picker-input', vueDateTimePicker);
import vueDateRangePicker from "./components/vueDateRangePicker.vue";
app.component('vue-date-range-picker-input', vueDateRangePicker);
import SearchableSelect from "./components/SearchableSelect.vue";
app.component('searchable-select', SearchableSelect);
import SearchableMultiSelect from "./components/searchableMultiSelect.vue";
app.component('searchable-multi-select', SearchableMultiSelect);
import Increment from "./components/Increment.vue";
app.component('increment', Increment);
/** /**
* The following block of code may be used to automatically register your * The following block of code may be used to automatically register your
* Vue components. It will recursively scan this directory for the Vue * Vue components. It will recursively scan this directory for the Vue

@ -0,0 +1,122 @@
<template>
<div id="currency">
<input @keyup="keyup" :id="xid" :placeholder="xtitle" :class="getClass" type="text" v-model="val">
<input type="hidden" :name="xname" :value="noComma">
</div>
</template>
<script>
function commafy(num) {
if (typeof num !== 'string') {
return '';
}
let str = uncommafy(num.toString()).split('.');
if (str[0].length >= 5) {
str[0] = str[0].replace(/(\d)(?=(\d{3})+$)/g, '$1,');
}
if (str[1] && str[1].length >= 5) {
str[1] = str[1].replace(/(\d{3})/g, '$1 ');
}
return str.join('.');
}
function uncommafy(txt) {
return txt.split(',').join('');
}
export default {
name: "curency-input",
components: {},
data: () => {
return {
val: ''
}
},
emits: ['update:modelValue'],
props: {
modelValue: {
type: [Number, String],
default: 'nop',
},
xname: {
default: "",
type: String,
},
xtitle: {
default: "",
type: String,
},
xvalue: {
default: "",
type: String,
},
xid: {
default: "",
type: String,
},
customClass: {
default: "",
type: String,
},
err: {
default: false,
type: Boolean,
},
updateKey: {
default: function () {
},
type: Function,
},
},
mounted() {
if (this.modelValue !== 'nop') {
if (typeof this.modelValue == 'number') {
this.val = commafy(this.modelValue.toString());
} else {
this.val = commafy(this.modelValue);
}
} else {
if (typeof this.xvalue == 'number') {
this.val = commafy(this.xvalue.toString());
} else {
this.val = commafy(this.xvalue);
}
}
},
computed: {
noComma: function () {
const n = uncommafy(this.val);
if (this.modelValue != null) {
this.$emit('update:modelValue', n);
}
return n;
},
getClass: function () {
if (this.err == true || (typeof this.err == 'String' && this.err.trim() == '1')) {
return 'form-control is-invalid ' + this.customClass;
}
return 'form-control ' + this.customClass;
},
},
methods: {
keyup: function () {
this.val = commafy(this.val);
if (typeof this.update === 'function') {
this.update(this.updateKey, parseInt(this.noComma));
}
},
}
}
</script>
<style scoped>
</style>

@ -1,23 +1,100 @@
<template> <template>
<div class="container"> <div id="example">
<div class="row justify-content-center"> <div class="p-2">
<div class="col-md-8"> <currency-input v-model="p"></currency-input>
<div class="card"> <span>
<div class="card-header">Example Component</div> {{ p }}
</span>
</div>
<div class="p-2">
<font-awesome-icon-picker v-model="f"></font-awesome-icon-picker>
<span>
{{ f }}
</span>
</div>
<div class="p-2">
<remix-icon-picker v-model="r"></remix-icon-picker>
<span>
{{ r }}
</span>
</div>
<div class="card-body"> <div class="p-2">
I'm an example component. <searchable-multi-select :items="cats" v-model="mss"></searchable-multi-select>
<span>
{{ mss }}
</span>
</div> </div>
<div class="p-2">
<vue-date-range-picker v-model="dpr"></vue-date-range-picker>
<span>
{{ dpr }}
</span>
</div> </div>
<div class="p-2">
<vue-date-time-picker v-model="dp"></vue-date-time-picker>
<span>
{{ dp }}
</span>
</div> </div>
<div class="p-2">
<vue-date-time-picker v-model="dpt" :timepicker="true"></vue-date-time-picker>
<span>
{{ dpt }}
</span>
</div> </div>
<div class="p-2">
<increment v-model="i"></increment>
<span>
{{ i }}
</span>
</div>
</div> </div>
</template> </template>
<script> <script>
import CurrencyInput from "./CurrencyInput.vue";
import fontAwesomeIconPicker from "./FontAwesomeIconPicker.vue";
import remixIconPicker from "./RemixIconPicker.vue";
import searchableSelect from "./SearchableSelect.vue";
import searchableMultiSelect from "./searchableMultiSelect.vue";
import vueDateTimePicker from "./vueDateTimePicker.vue";
import vueDateRangePicker from "./vueDateRangePicker.vue";
import Increment from "./Increment.vue";
export default { export default {
mounted() { name: "example",
console.log('Component mounted.') components: {
CurrencyInput, fontAwesomeIconPicker, remixIconPicker,Increment,
searchableSelect, searchableMultiSelect, vueDateRangePicker, vueDateTimePicker
},
data: () => {
return {
p: 100000,
f: 'fa fa-user',
r: 'ri-user-line',
ss: 3,
mss: [3,11,7],
dpr:[],
dp: null,
dpt: null,
i:7,
} }
},
props: {
cats: {
default: [],
}
},
mounted() {
},
computed: {},
methods: {}
} }
</script> </script>
<style scoped>
#example {
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,105 @@
<template>
<div id="inc">
<div class="btn btn-outline-danger" @click="dec">
<i class="ri-subtract-line"></i>
</div>
<input type="text" class="form-control"
v-model="val"
@keyup.up="inc"
@keyup.down="dec">
<div class="btn btn-outline-success" @click="inc">
<i class="ri-add-line"></i>
</div>
</div>
</template>
<script>
export default {
name: "inc",
components: {},
data: () => {
return {
val: 0,
}
},
emits: ['update:modelValue'],
props: {
modelValue: {
type: [Number, NaN],
default: NaN,
},
xvalue: {
type: Number,
default: 0
},
xmin: {
type: Number,
default: 0
},
xmax: {
type: Number,
default: 100
},
},
mounted() {
if (!isNaN(this.modelValue)) {
this.val = this.modelValue;
}else{
this.val = this.xvalue;
}
},
computed: {},
methods: {
inc(e){
e.preventDefault();
if (this.val < this.xmax){
this.val++;
}
},
dec(e){
e.preventDefault();
if (this.val > this.xmin){
this.val--;
}
}
},
watch: {
val(newValue) {
if (parseInt(this.val) > this.xmax){
this.val = this.xmax;
}
if (parseInt(this.val) < this.xmin){
this.val = this.xmin;
}
if (!isNaN(this.modelValue)) {
this.$emit('update:modelValue', newValue);
}
}
}
}
</script>
<style scoped>
#inc {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
direction: ltr;
}
#inc .form-control {
border-radius: 0;
text-align: center;
}
#inc div:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
user-select: none;
}
#inc div:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
user-select: none;
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,199 @@
<template>
<div id="searchable-select">
<div id="ss-modal" @click.self="hideModal" v-if="modalShow">
<div id="ss-selector">
<div class="p-2">
<input type="text" class="form-control" v-model="q" :placeholder="xtitle">
</div>
<div class="p-2">
<ul id="vue-search-list" class="list-group list-group-flush">
<template v-for="item in items">
<li
v-if="(q != '' && item[titleField].indexOf(q) != -1) || (q == '')"
@click="selecting(item[valueField])"
:class="`list-group-item ${val == item[valueField]?'selected':''}`">
{{item[titleField]}}
</li>
</template>
</ul>
</div>
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend" id="vue-search-btn" @click="showModal" >
<span class="input-group-text" id="basic-addon1">
<i class="ri-search-2-line"></i>
</span>
</div>
<select :id="xid" :class="getClass" v-model="val" @change="select">
<option value=""> {{xtitle}} </option>
<option v-for="item in items"
:value="item[valueField]"
:selected="item[valueField] == val">
{{item[titleField]}}
</option>
</select>
</div>
</div>
<input type="hidden" :name="xname" :value="val">
</template>
<script>
export default {
name: "searchable-select",
components: {},
data: () => {
return {
modalShow: false, // modal handle
q: '', // search query
val:'',
}
},
emits: ['update:modelValue'],
props: {
modelValue: {
default: NaN,
},
items:{
required: true,
default: [],
type: Array,
},
valueField:{
default: 'id',
type: String,
},
titleField:{
default: 'title',
type: String,
},
xname:{
default: "",
type: String,
},
xtitle:{
default: "Please select",
type: String,
},
xvalue:{
default: "",
type: String,
},
xid:{
default: "",
type: String,
},
customClass:{
default: "",
type: String,
},
err:{
default: false,
type: Boolean,
},
onSelect:{
default: function () {
},
type: Function,
},
closeOnSelect: {
default: false,
type: Boolean,
},
},
mounted() {
if (!isNaN(this.modelValue)) {
this.val = this.modelValue;
}else{
this.val = this.xvalue;
}
},
computed: {
getClass: function () {
if (this.err == true || ( typeof this.err == 'String' && this.err.trim() == '1' )) {
return 'form-control is-invalid '+this.customClass;
}
return 'form-control '+this.customClass;
},
},
methods: {
selecting(i){
this.val = i;
this.onSelect(this.val);
if (this.closeOnSelect){
this.hideModal();
}
},
select(){
this.onSelect(this.val);
},
hideModal:function () {
this.modalShow = false;
},
showModal(){
this.modalShow = true;
}
},
watch: {
val(newValue) {
if (!isNaN(this.modelValue)) {
this.$emit('update:modelValue', newValue);
}
}
}
}
</script>
<style scoped>
#searchable-select {
}
#vue-search-btn{
cursor: pointer;
user-select: none;
}
#vue-search-btn:hover .input-group-text{
background: darkred;
}
#ss-modal{
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
background: #00000033;
backdrop-filter: blur(4px);
user-select: none;
}
#ss-selector{
height: 60vh;
border-radius: 7px;
min-width: 350px;
width: 400px;
max-width: 90%;
margin: 20vh auto;
background: #ffffff99;
padding: 5px;
}
#vue-search-list{
height: calc(60vh - 90px);
overflow-x: auto ;
}
#vue-search-list .list-group-item:hover{
background: deepskyblue;
}
#vue-search-list .list-group-item.selected{
background: dodgerblue;
color: white;;
}
</style>

@ -0,0 +1,209 @@
class PersianDate {
persianMonthNames = ['', 'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'];
settings= {
gSpliter: '/',
}
/**
* from parsi date by mobin ghasem pour
* @param {integer} year
* @returns {Boolean}
*/
isLeapYear = function (year) {
if (((year % 4) === 0 && (year % 100) !== 0) || ((year % 400) === 0) && (year % 100) === 0)
return true;
else
return false;
};
parseHindi = function (str) {
let r = str.toString();
let org = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
let hindi = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
for (var ch in org) {
r = r.replace(new RegExp(org[ch], 'g'), hindi[ch]);
}
return r;
}
exploiter = function (date_txt, determ) {
if (typeof determ === 'undefined') {
determ = '/';
}
let a = date_txt.split(determ);
if (typeof a[2] === 'undefined') {
return a;
}
if (a[0].length < a[2].length) {
return [a[2], a[1], a[0]];
}
return a;
};
imploiter = function (date_txt, determ) {
if (determ === undefined) {
determ = '/';
}
return date_txt[0] + determ + date_txt[1] + determ + date_txt[2];
};
/**
* from parsi date by mobin ghasem pour
* @param {Array} indate
* @returns {Array}
*/
persian2Gregorian = function (indate) {
let jy = indate[0];
let jm = indate[1];
let jd = indate[2];
let gd;
let j_days_sum_month = [0, 0, 31, 62, 93, 124, 155, 186, 216, 246, 276, 306, 336, 365];
let g_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let g_days_leap_month = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
gd = j_days_sum_month[parseInt(jm)] + parseInt(jd);
let gy = parseInt(jy) + 621;
if (gd > 286)
gy++;
if (this.isLeapYear(gy - 1) && 286 < gd)
gd--;
if (gd > 286)
gd -= 286;
else
gd += 79;
let gm;
if (this.isLeapYear(gy)) {
for (gm = 0; gd > g_days_leap_month[gm]; gm++) {
gd -= g_days_leap_month[gm];
}
} else {
for (gm = 0; gd > g_days_in_month[gm]; gm++)
gd -= g_days_in_month[gm];
}
gm++;
if (gm < 10)
gm = '0' + gm;
gd = gd < 10 ? '0'+gd: gd;
return [gy, gm, gd];
};
/**
* from parsi date by mobin ghasem pour
* @param {Array} indate
* @returns {Array}
*/
gregorian2Persian = function (indate) {
let gy, gm, gd, j_days_in_month, g_days_sum_month, dayofyear, leab, leap, jd, jy, jm, i;
gy = indate[0];
gm = indate[1];
gd = indate[2];
j_days_in_month = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
g_days_sum_month = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
dayofyear = g_days_sum_month[parseInt(gm)] + parseInt(gd);
leab = this.isLeapYear(gy);
leap = this.isLeapYear(gy - 1);
if (dayofyear > 79) {
jd = (leab ? dayofyear - 78 : dayofyear - 79);
jy = gy - 621;
for (i = 0; jd > j_days_in_month[i]; i++) {
jd -= j_days_in_month[i];
}
} else {
jd = ((leap || (leab && gm > 2)) ? 287 + dayofyear : 286 + dayofyear);
jy = gy - 622;
if (leap == 0 && jd == 366)
return [jy, 12, 30];
for (i = 0; jd > j_days_in_month[i]; i++) {
jd -= j_days_in_month[i];
}
}
jm = ++i;
jm = (jm < 10 ? jm = '0' + jm : jm);
if (jm == 13) {
jm = 12;
jd = 30;
}
if (jm.toString().length == 1) {
jm = '0' + jm;
}
if (jd.toString().length == 1) {
jd = '0' + jd;
}
return [jy.toString(), jm, jd];
};
convertDate2Persian = function (dt){
let tmp = dt.toLocaleString('fa-IR-u-nu-latn')
.split('،')
.join(',');
let date = tmp.split(',');
let result = date[0].split('/');
result[1] = this.make2number(result[1]);
result[2] = this.make2number(result[2]);
return result;
}
make2number = function (instr) {
let num = instr.toString();
return num.length === 2 ? num : '0' + num;
}
gDate2Timestamp = function (stri) {
return Math.round(new Date(stri + " 00:00:00").getTime() / 1000);
}
gTimestamp2Date = function (unix_timestamp) {
let date = new Date(unix_timestamp * 1000);
return date.getFullYear() + settings.gSpliter + date.getMonth() + 1 + settings.gSpliter + date.getDate();
}
pDate2Timestamp = function (stri) {
return this.gDate2Timestamp(this.imploiter(this.persian2Gregorian(this.exploiter(stri))));
}
pTimestamp2Date = function (unix_timestamp) {
let date = new Date(unix_timestamp * 1000);
return this.imploiter(this.gregorian2Persian([date.getFullYear(), date.getMonth() + 1, date.getDate()]));
}
getPersianWeekDay = function (jdate) {
let tmp = this.imploiter(this.persian2Gregorian(this.exploiter(jdate)), '/');
let dd = new Date(tmp + " 00:00:00").getDay() + 1;
if (dd > 6) {
dd -= 7;
}
return dd;
};
pGetLastDayMonth = function (mn, yr) {
let tmp;
let last = 29;
let now = this.pDate2Timestamp(yr + '/' + mn + '/' + (29));
for (let i = 1; i < 4; i++) {
now += 86400;
tmp = this.exploiter(this.pTimestamp2Date(now));
if (tmp[2] < last) {
return last;
} else {
last = tmp[2];
}
}
return last;
}
}
export default PersianDate;

@ -0,0 +1,229 @@
<template>
<div id="searchable-select">
<div id="ss-modal" @click.self="hideModal" v-if="modalShow">
<div id="ss-selector">
<div class="p-2">
<input type="text" class="form-control" v-model="q" :placeholder="xtitle">
</div>
<div class="p-2">
<ul id="vue-search-list" class="list-group list-group-flush">
<template v-for="item in items">
<li
v-if="(q != '' && item[titleField].toLocaleLowerCase().indexOf(q.toLocaleLowerCase()) != -1) || (q == '')"
@click="selecting(item[valueField])"
:class="`list-group-item ${val.indexOf(item[valueField]) !== -1?'selected':''}`">
{{ item[titleField] }}
</li>
</template>
</ul>
</div>
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend" id="vue-search-btn" @click="showModal">
<span class="input-group-text" id="basic-addon1">
<i class="ri-check-line"></i>
</span>
</div>
<div class="form-control" id="vue-lst" @click.self="showModal">
<template v-for="item in items">
<span class="tag-select" v-if=" val.indexOf(item[valueField]) !== -1" >
{{ item[titleField] }}
<i class="ri-close-line" @click="rem(item[valueField])"></i>
</span>
</template>
</div>
</div>
</div>
<input type="hidden" :name="xname" :value="JSON.stringify(val)">
</template>
<script>
export default {
name: "searchable-select",
components: {},
data: () => {
return {
modalShow: false, // modal handle
q: '', // search query
val: [],
}
},
emits: ['update:modelValue'],
props: {
modelValue: {
default: 'nop',
},
items: {
required: true,
default: [],
type: Array,
},
valueField: {
default: 'id',
type: String,
},
titleField: {
default: 'title',
type: String,
},
xname: {
default: "",
type: String,
},
xtitle: {
default: "Please select",
type: String,
},
xvalue: {
default: [],
type: Array,
},
xid: {
default: "",
type: String,
},
customClass: {
default: "",
type: String,
},
err: {
default: false,
type: Boolean,
},
onSelect: {
default: function () {
},
type: Function,
},
},
mounted() {
if (this.modelValue != 'nop') {
this.val = this.modelValue;
}else{
this.val = this.xvalue;
}
},
computed: {
getClass: function () {
if (this.err == true || (typeof this.err == 'String' && this.err.trim() == '1')) {
return 'form-control is-invalid ' + this.customClass;
}
return 'form-control ' + this.customClass;
},
},
methods: {
rem(i){
this.val.splice(this.val.indexOf(i),1);
this.onSelect(this.val,i);
},
selecting(i) {
if (this.val.indexOf(i) == -1){
this.val.push(i);
}else{
this.val.splice(this.val.indexOf(i),1);
}
this.onSelect(this.val,i);
},
select() {
this.onSelect(this.val);
},
hideModal: function () {
this.modalShow = false;
},
showModal() {
this.modalShow = true;
}
},
watch: {
val(newValue) {
if (this.modelValue != 'nop') {
this.$emit('update:modelValue', newValue);
}
}
}
}
</script>
<style scoped>
#searchable-select {
}
#vue-search-btn {
cursor: pointer;
user-select: none;
}
#vue-search-btn:hover .input-group-text {
background: deepskyblue;
}
#ss-modal {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
background: #00000033;
backdrop-filter: blur(4px);
user-select: none;
}
#ss-selector {
height: 60vh;
border-radius: 7px;
min-width: 350px;
width: 400px;
max-width: 90%;
margin: 20vh auto;
background: #282D47;
box-shadow: 0 0 4px gray;
padding: 5px;
}
#vue-search-list {
height: calc(60vh - 90px);
overflow-x: auto;
}
#vue-search-list .list-group-item:hover {
background: #6610F2;
}
#vue-search-list .list-group-item.selected {
background: darkred;
color: white;;
}
#vue-lst {
user-select: none;
white-space: nowrap;
overflow: hidden;
}
.tag-select{
display: inline-block;
padding: 0 4px 0 20px ;
margin-right: 5px;
background: #282c34dd;
color: white;
position: relative;
border-radius: 3px;
}
.tag-select i{
font-size: 20px;
position: absolute;
left: 0;
top: -5px;
}
.tag-select i:hover{
color: red;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,374 @@
<template>
<div id="vue-a1-calendar">
<div v-if="pDate !== null">
<div class="cal-grid">
<div @click="prvMonth">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M10.0859 12.0001L5.29297 16.793L6.70718 18.2072L12.9143 12.0001L6.70718 5.79297L5.29297 7.20718L10.0859 12.0001ZM17.0001 6.00008L17.0001 18.0001H15.0001L15.0001 6.00008L17.0001 6.00008Z"></path>
</svg>
</div>
<div>
{{ pDate.persianMonthNames[cMonth] }}
</div>
<div>
<input type="number" min="0" v-model="cYear">
</div>
<div @click="nxtMonth">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M13.9142 12.0001L18.7071 7.20718L17.2929 5.79297L11.0858 12.0001L17.2929 18.2072L18.7071 16.793L13.9142 12.0001ZM7 18.0001V6.00008H9V18.0001H7Z"></path>
</svg>
</div>
</div>
<table id="vue-cal-table">
<tr>
<th @click="selectByWeekDay(0)">
{{ __("Saturday") }}
</th>
<th @click="selectByWeekDay(1)">
{{ __("Sunday") }}
</th>
<th @click="selectByWeekDay(2)">
{{ __("Monday") }}
</th>
<th @click="selectByWeekDay(3)">
{{ __("Tuesday") }}
</th>
<th @click="selectByWeekDay(4)">
{{ __("Wednesday") }}
</th>
<th @click="selectByWeekDay(5)">
{{ __("Thursday") }}
</th>
<th @click="selectByWeekDay(6)">
{{ __("Friday") }}
</th>
</tr>
<tr v-for="(tr,i) in monthArray">
<td v-for="(td,j) in tr" @mouseenter="hover" @click="selecting(td)" :class="getClass(td)" :title="td.date+' '+td.pDate"
:data-timpstamp="td.unixTimeStamp">
{{ td.text }}
<template v-if="texts[fixDate(td.date)] !== undefined">
<div class="a1-badge" v-html="texts[fixDate(td.date)]"></div>
</template>
</td>
</tr>
</table>
<input type="hidden" :value="JSON.stringify(selected)" :name="inputName">
</div>
<!-- <div>-->
<!-- {{JSON.stringify(selected)}}-->
<!-- </div>-->
<div class="text-center mb-2">
&nbsp;{{ currentDate }}&nbsp;
</div>
</div>
</template>
<script>
import persianDate from './libs/persian-date.js';
export default {
name: "vue-a1-calendar",
components: {},
data: () => {
return {
currentDate: '',
pDate: null,
cMonth: 4, // current month
cYear: 1402, // current year
selected: [],
selectedTimeStamp: [],
// translate simple
strings: {
Monday: 'دوشنبه',
Tuesday: 'سه‌شنبه',
Wednesday: 'چهارشنبه',
Thursday: 'پنجشنبه',
Friday: 'آدینه',
Saturday: 'شنبه',
Sunday: 'یکشنبه'
}
}
},
props: {
month: {
default: 4,
type: Number,
},
year: {
default: 1370,
type: Number,
},
texts: {
default: {},
type: Object,
},
onSelect: {
type: Function,
default: function () {
},
},
inputName: {
type: String,
default: 'vueCalendarValue'
},
},
mounted() {
// initial values
this.cYear = this.year;
this.cMonth = this.month;
this.pDate = new persianDate();
}
,
computed: {
monthStart() {
return this.pDate.getPersianWeekDay(this.cYear + '/' + this.cMonth + '/1');
},
monthArray() {
let r = [[]];
let counter = this.monthStart;
for (let i = 1; i <= this.monthStart; i++) {
r[0].push({
date: '',
text: '',
unixTimeStamp: '',
pDate: '',
});
}
for (let i = 1; i <= this.pDate.pGetLastDayMonth(this.cMonth, this.cYear); i++) {
counter++;
r[r.length - 1].push({
date: this.pDate.imploiter(this.pDate.persian2Gregorian([this.cYear, this.cMonth, i]), '-'),
text: this.pDate.parseHindi(i),
unixTimeStamp: this.pDate.gDate2Timestamp(this.pDate.persian2Gregorian([this.cYear, this.cMonth, i])),
pDate: this.pDate.parseHindi(this.cYear + '/' + this.cMonth + '/' + this.pDate.make2number(i)),
});
if (counter % 7 === 0) {
r.push([]);
}
}
return r;
}
,
}
,
methods: {
/**
* Maybe one day translator
* @param txt
* @returns {*}
* @private
*/
__(txt) {
return this.strings[txt];
}
,
indexSelected(td) {
return this.selectedTimeStamp.indexOf(td.unixTimeStamp);
}
,
/**
* td object toggle select
* @param td
*/
selecting(td) {
if (td.date === '') {
return false;
}
if (this.indexSelected(td) === -1) {
this.selected.push(td);
this.selectedTimeStamp.push(td.unixTimeStamp);
this.onSelect(td);
} else {
this.selected.splice(this.indexSelected(td), 1);
this.selectedTimeStamp.splice(this.indexSelected(td), 1);
}
},
/**
* calculate class by props of td
* @param td
* @returns {string}
*/
getClass(td) {
let cls = '';
if (td.text === '') {
return '';
} else {
cls += ' selectable-td';
}
if (this.indexSelected(td) !== -1) {
cls += ' selected-td';
}
return cls;
}
,
/**
* Selected dates by day week
* @param i
*/
selectByWeekDay(i) {
for (const tr of this.monthArray) {
if (tr[i] !== undefined) {
this.selecting(tr[i]);
}
}
},
hover: function (e) {
this.currentDate = e.target.getAttribute('title');
},
nxtMonth() {
if (this.cMonth === 12) {
this.cYear++;
this.cMonth = 1;
} else {
this.cMonth++;
}
},
prvMonth() {
if (this.cMonth === 1) {
this.cYear--;
this.cMonth = 12;
} else {
this.cMonth--;
}
},
fixDate(d) {
let t = d.split('-');
return parseInt(t[0]) + '-' + (parseInt(t[1]) < 10 ? '0' + parseInt(t[1]) : parseInt(t[1])) + '-' + (parseInt(t[2]) < 10 ? '0' + parseInt(t[2]) : parseInt(t[2]));
},
}
}
</script>
<style scoped>
#vue-a1-calendar {
border: 1px solid #44444444;
background: #fff;
direction: rtl;
user-select: none;
font-family: 'Vazir', 'Vazirmatn', sans-serif;
}
#vue-cal-table {
width: calc(100% - 2rem);
border-spacing: 0;
text-align: center;
margin: 0 auto 1rem;
}
#vue-cal-table th {
cursor: pointer;
background: dodgerblue;
border: 1px solid navy;
border-right: 0;
color: white;
width: calc(100% / 7);
}
#vue-cal-table td, #vue-cal-table th {
font-size: 22px;
padding: 1rem;
}
#vue-cal-table th:first-child, #vue-cal-table td:first-child {
border-right: 1px solid navy;
}
#vue-cal-table td {
border: 1px solid navy;
border-right: 0;
border-top: 0;
position: relative;
height: 4rem;
transition: 200ms;
}
#vue-cal-table td.selected-td {
background: rgba(144, 238, 144, 0.82);
}
#vue-cal-table td.selectable-td {
cursor: pointer;
}
.cal-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 1rem;
background: #00a2a2;
color: white;
border-radius: 5px;
}
.cal-grid div {
text-align: center;
padding: 1rem;
font-size: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.cal-grid svg {
width: 40px;
fill: white;
}
.cal-grid div:first-child, .cal-grid div:last-child {
cursor: pointer;
}
.cal-grid input {
font-size: 25px;
text-align: center;
width: 100px;
background: transparent;
border: none;
outline: none;
color: white;
}
.a1-badge {
background: dodgerblue;
color: white;
border-radius: 3px;
position: absolute;
left: 2px;
top: 2px;
font-size: 14px;
padding: 7px;
opacity: .85;
transition: 400ms;
}
td.selectable-td:hover {
background: #11111111;
}
td.selectable-td.selected-td:hover {
background: rgba(25, 107, 73, .5) !important;
}
td:hover .a1-badge {
opacity: 1;
}
</style>

@ -0,0 +1,7 @@
document.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-confirm')) {
if (!confirm('Are you sure you want to delete this item?')) { // WIP Need to translate
e.preventDefault();
}
}
});

@ -4,6 +4,42 @@ function clearSelection()
else if (document.selection) {document.selection.empty();} else if (document.selection) {document.selection.empty();}
} }
function serializeForm(selector) {
let form = document.querySelector(selector);
let formData = new FormData(form);
let serializedArray = [];
formData.forEach(function(value, key) {
serializedArray.push({
name: key,
value: value
});
});
return serializedArray;
}
function handleCheckChange () {
let frm = serializeForm('#main-form');
let bi = document.querySelector('#bulk-idz');
bi.innerHTML = '';
for( const item of frm) {
let n =document.createElement("input");
n.name = item.name;
n.value = item.value;
n.type = 'hidden';
bi.appendChild(n);
}
if (frm.length == 0){
document.querySelector('#bulk-from').style.maxHeight = '0';
}else{
document.querySelector('#bulk-from').style.maxHeight = '250px';
}
}
window.addEventListener('load',function () { window.addEventListener('load',function () {
let chkall = document.querySelectorAll(".chkall"); let chkall = document.querySelectorAll(".chkall");
@ -18,6 +54,7 @@ window.addEventListener('load',function () {
checkbox.removeAttribute("checked"); checkbox.removeAttribute("checked");
} }
}); });
handleCheckChange();
}); });
// Attach an event listener for "change" and "click" events // Attach an event listener for "change" and "click" events
chkall.forEach(function(chkall) { chkall.forEach(function(chkall) {
@ -44,6 +81,7 @@ window.addEventListener('load',function () {
checkbox.removeAttribute("checked"); checkbox.removeAttribute("checked");
}); });
} }
handleCheckChange();
} }
@ -55,6 +93,7 @@ window.addEventListener('load',function () {
chkboxes.forEach(chkbox => { chkboxes.forEach(chkbox => {
chkbox.addEventListener('click', handleCheckboxClick); chkbox.addEventListener('click', handleCheckboxClick);
chkbox.parentNode.querySelector('label').addEventListener('click', handleCheckboxClick); chkbox.parentNode.querySelector('label').addEventListener('click', handleCheckboxClick);
chkbox.addEventListener('change',handleCheckChange);
}); });
function handleCheckboxClick(e) { function handleCheckboxClick(e) {
@ -80,9 +119,10 @@ window.addEventListener('load',function () {
} }
handleCheckChange();
lastChecked = self; lastChecked = self;
} }
handleCheckChange();
}); });

@ -1,5 +1,61 @@
$pnl-brc-height: 50px;
#panel-breadcrumb{ #panel-breadcrumb{
height: 50px; height: $pnl-brc-height;
background: #00000033; background: #282d46;
margin-bottom: 1rem; margin-bottom: 1rem;
ul{
padding: 0;
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
list-style: none;
//border: 1px solid black;
border-radius: 4px ;
transition: .5s;
li{
position: relative;
overflow: hidden;
a,span{
height: $pnl-brc-height;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1rem;
}
i{
font-size: 25px;
margin: 0 .5rem;
}
&:before {
content: "";
width: 37px;
height: 37px;
border: 1px solid black;
transform: rotate(45deg);
position: absolute;
inset-inline-start: -1.05rem;
background: #282d46;
top: 7px;
}
&:first-child:before {
display: none;
}
&:hover{
background: #444e7c;
background-image: url("../images/pattern.png");
& + li:before{
background: #444e7c;
background-image: url("../images/pattern.png");
}
}
}
}
} }

@ -47,7 +47,7 @@ a.btn,a.action-btn,a.circle-btn{
color: white; color: white;
} }
position: fixed; position: fixed;
inset-inline-end: 1rem; inset-inline-start: 4.2rem;
bottom: 1rem; bottom: 1rem;
} }

@ -10,7 +10,15 @@
font-weight: 200; font-weight: 200;
margin: 0; margin: 0;
} }
h2,h3,h4,h5,h6{
font-weight: 200;
font-size: 22px;
margin-bottom: 0;
}
h2{
margin-bottom: .5rem;
}
.item-search { .item-search {
form { form {
padding: 1rem; padding: 1rem;
@ -28,6 +36,8 @@
} }
td,th{ td,th{
transition: 300ms;
transition-delay: 100ms;
&:first-child{ &:first-child{
width: 100px; width: 100px;
} }
@ -66,9 +76,10 @@
&:hover{ &:hover{
td{
background-color: #00000044;
background-image: url("../images/pattern.png"); background-image: url("../images/pattern.png");
background-color: #00000044;
td{
background-color: transparent;
} }
} }
@ -84,3 +95,10 @@
} }
#bulk-from{
max-height: 0;
overflow: hidden;
box-sizing: border-box;
transition: 1s;
}

@ -1,5 +1,5 @@
#panel-navbar { #panel-navbar {
height: calc(100vh - 55px); height: calc(100vh - 60px);
overflow-y: auto; overflow-y: auto;
position: relative; position: relative;
@ -72,3 +72,8 @@
} }
} }
#panel-top-navbar{
background: rgb(40, 45, 70);
padding-top: .8rem;
}

@ -5,8 +5,8 @@
<div class="row"> <div class="row">
{{-- list side bar start--}} {{-- list side bar start--}}
<div class="col-xl-3 mb-3"> <div class="col-xl-3">
<div class="item-list"> <div class="item-list mb-3">
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
<h1> <h1>
@ -15,7 +15,7 @@
</div> </div>
<div class="col-4 pt-3 text-end"> <div class="col-4 pt-3 text-end">
@if(hasRoute('trashed')) @if(hasRoute('trashed'))
<a class="btn btn-outline-danger me-2" <a class="btn btn-outline-danger me-3"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-placement="top"
data-bs-custom-class="custom-tooltip" data-bs-custom-class="custom-tooltip"
@ -27,24 +27,82 @@
@endif @endif
</div> </div>
</div> </div>
<form action="" class="p-2"> <form action="" class="p-3">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" placeholder="{{__("Search")}}..." <span class="btn btn-outline-secondary" type="button" id="button-addon2">
aria-label="{{__("Search")}}..." aria-describedby="button-addon2">
<button class="btn btn-outline-secondary" type="button" id="button-addon2">
<i class="ri-search-2-line"></i> <i class="ri-search-2-line"></i>
</button> </span>
<input type="text" name="q" class="form-control" placeholder="{{__("Search")}}..."
aria-label="{{__("Search")}}..." aria-describedby="button-addon2"
value="{{request()->input('q','')}}">
</div> </div>
@yield('filter') @yield('filter')
<button class="btn btn-primary w-100">
{{__("Search & Filter")}}
</button>
</form> </form>
</div> </div>
<div class="item-list mb-3 py-3">
<div class="grid-equal text-center p-1">
<span>
{{__("Totol")}}
</span>
<span>
({{$items->total()}})
</span>
</div>
<hr>
<div class="grid-equal text-center p-1">
<span>
{{__("From - To")}}
</span>
<span>
@paginated($items)
</span>
</div>
</div>
@if(hasRoute('bulk'))
<div class="item-list mb-3">
<h3 class="p-3">
<i class="ri-check-double-line"></i>
{{__("Bulk actions:")}}
</h3>
<form action="{{getRoute('bulk',[],strpos(request()->url(),'trashed') == false?'index':'trashed')}}" id="bulk-from" method="post">
<div class="p-3">
@csrf
<select class="form-control mb-3" name="action" required>
<option value=""></option>
@if(strpos(request()->url(),'trashed') != false)
<option value="restore"> {{__("Batch restore")}} </option>
@else
<option value="delete"> {{__("Batch delete")}} </option>
@endif
@yield('bulk')
</select>
<button class="btn btn-primary w-100">
{{__("Do it")}}
</button>
<div id="bulk-idz"></div>
</div>
</form>
</div>
@endif
@include('components.err')
</div> </div>
{{-- list side bar end--}} {{-- list side bar end--}}
{{-- list content start--}} {{-- list content start--}}
<div class="col-xl-9"> <div class="col-xl-9 ps-xl-0">
<div class="item-list"> <form class="item-list" id="main-form">
<table class="table-list"> <table class="table-list">
<thead> <thead>
@ -68,9 +126,8 @@
</th> </th>
@endforeach @endforeach
@yield('table-head') @yield('table-head')
<th class="text-center"> <th>
{{__("Totol")}}:
{{$items->total()}}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -79,7 +136,7 @@
<tr> <tr>
<td> <td>
<input type="checkbox" id="chk-{{$item->id}}" class="chkbox" <input type="checkbox" id="chk-{{$item->id}}" class="chkbox"
name="id[{{$item->id}}]"> name="id[{{$item->id}}]" value="{{$item->id}}">
<label for="chk-{{$item->id}}"> <label for="chk-{{$item->id}}">
{{$item->id}} {{$item->id}}
</label> </label>
@ -87,7 +144,7 @@
@foreach($cols as $k => $col) @foreach($cols as $k => $col)
@if($k == 0 && hasRoute('edit')) @if($k == 0 && hasRoute('edit'))
<td> <td>
<a href="{{getRoute('edit',$item->id)}}"> <a href="{{getRoute('edit',$item->{$item->getRouteKeyName()})}}">
<b> <b>
{{$item->name}} {{$item->name}}
</b> </b>
@ -101,37 +158,74 @@
@endforeach @endforeach
@yield('table-body') @yield('table-body')
<td> <td>
@if(hasRoute('destroy'))
<a href="{{getRoute('destroy',$item->id)}}" @if(strpos(request()->url(),'trashed') != false && hasRoute('restore'))
class="btn btn-outline-danger btn-sm mx-1" <a href="{{getRoute('restore',$item->{$item->getRouteKeyName()},'trashed')}}"
class="btn btn-success btn-sm mx-1 d-xl-none d-xxl-none"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-placement="top"
data-bs-custom-class="custom-tooltip" data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Remove")}}"> data-bs-title="{{__("Restore")}}">
<i class="ri-close-line"></i> <i class="ri-recycle-line"></i>
</a>
@else
<div class="dropdown d-xl-none d-xxl-none">
<a class="btn btn-outline-secondary dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
</a>
<ul class="dropdown-menu">
@foreach($buttons as $btn => $btnData)
<li>
<a class="dropdown-item {{$btnData['class']}}"
href="{{getRoute($btn,$item->id)}}">
<i class="{{$btnData['icon']}}"></i>
&nbsp;
{{__($btnData['title'])}}
</a> </a>
</li>
@endforeach
</ul>
</div>
@endif @endif
@if(hasRoute('edit')) <div class="d-none d-xl-block d-xxl-block">
<a href="{{getRoute('edit',$item->id)}}" @foreach($buttons as $btn => $btnData)
class="btn btn-outline-primary btn-sm mx-1"
@if(strpos($btnData['class'],'delete') == false )
@if(strpos(request()->url(),'trashed') == false)
<a href="{{getRoute($btn,$item->{$item->getRouteKeyName()})}}"
class="btn {{$btnData['class']}} btn-sm mx-1"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-placement="top"
data-bs-custom-class="custom-tooltip" data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Edit")}}"> data-bs-title="{{__($btnData['title'])}}">
<i class="ri-edit-2-line"></i> <i class="{{$btnData['icon']}}"></i>
</a> </a>
@endif @endif
@if(hasRoute('show')) @else
<a href="{{getRoute('show',$item->id)}}" @if( hasRoute('restore') && $item->trashed())
class="btn btn-outline-secondary btn-sm mx-1" <a href="{{getRoute('restore',$item->{$item->getRouteKeyName()},'trashed')}}"
class="btn btn-success btn-sm mx-1"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-placement="top"
data-bs-custom-class="custom-tooltip" data-bs-custom-class="custom-tooltip"
data-bs-title="{{__("Show")}}"> data-bs-title="{{__("Restore")}}">
<i class="ri-eye-line"></i> <i class="ri-recycle-line"></i>
</a>
@else
<a href="{{getRoute($btn,$item->{$item->getRouteKeyName()})}}"
class="btn {{$btnData['class']}} btn-sm mx-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="{{__($btnData['title'])}}">
<i class="{{$btnData['icon']}}"></i>
</a> </a>
@endif @endif
@yield('list-btn') @endif
@endforeach
</div>
</td> </td>
</tr> </tr>
@endforeach @endforeach
@ -158,13 +252,6 @@
{{$items->withQueryString()->links()}} {{$items->withQueryString()->links()}}
</div> </div>
<div class="col-md-3 text-center"> <div class="col-md-3 text-center">
<div class="p-2" data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="({{__("From - To - Total")}})">
@paginated($items)
</div>
</div> </div>
</div> </div>
</th> </th>

@ -8,5 +8,20 @@
{{__("Users list")}} | {{__("Users list")}} |
@endsection @endsection
@section('filter') @section('filter')
<h2>
<i class="ri-shield-check-line"></i>
{{__("Role filter")}}:
</h2>
<searchable-multi-select
:items='{{arrayNormolizeVueCompatible(\App\Models\User::$roles, true)}}'
title-field="name"
value-field="name"
xname="filter[role]"
:xvalue='{{request()->input('filter.role','[]')}}'
:close-on-Select="true"></searchable-multi-select>
@endsection
@section('bulk')
@foreach(\App\Models\User::$roles as $role)
<option value="role.{{$role}}"> {{__("Set")}} {{__("$role")}} </option>
@endforeach
@endsection @endsection

@ -0,0 +1,10 @@
@if(session()->has('message'))
<div class="alert alert-info">
{{ session()->get('message') }}
</div>
@endif
@foreach($errors->all() as $err)
<div class="alert alert-danger">
{{$err}}
</div>
@endforeach

@ -1,3 +1,28 @@
<nav id="panel-breadcrumb"> <nav id="panel-breadcrumb">
1 <ul>
<li>
<a href="#1">
<i class="ri-home-3-line"></i>
{{config('app.name')}}
</a>
</li>
<li>
<a href="#2">
<i class="ri-dashboard-3-line"></i>
{{__("Dashboard")}}
</a>
</li>
<li>
<a href="#3">
<i class="ri-user-3-line"></i>
{{__("Users")}}
</a>
</li>
<li>
<span>
<i class="ri-add-line"></i>
{{__("Add new user")}}
</span>
</li>
</ul>
</nav> </nav>

@ -1,4 +1,4 @@
<nav class="navbar navbar-expand-md shadow-sm"> <nav class="navbar navbar-expand-md shadow-sm" id="panel-top-navbar">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url('/') }}"> <a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }} {{ config('app.name', 'Laravel') }}

@ -20,10 +20,12 @@ Route::prefix(config('app.panel.prefix'))->name('admin.')->group(
Route::get('', [\App\Http\Controllers\Admin\UserController::class, 'index'])->name('index'); Route::get('', [\App\Http\Controllers\Admin\UserController::class, 'index'])->name('index');
Route::get('create', [\App\Http\Controllers\Admin\UserController::class, 'create'])->name('create'); Route::get('create', [\App\Http\Controllers\Admin\UserController::class, 'create'])->name('create');
Route::post('store', [\App\Http\Controllers\Admin\UserController::class, 'store'])->name('store'); Route::post('store', [\App\Http\Controllers\Admin\UserController::class, 'store'])->name('store');
Route::get('edit/{user}', [\App\Http\Controllers\Admin\UserController::class, 'edit'])->name('edit'); Route::get('edit/{item}', [\App\Http\Controllers\Admin\UserController::class, 'edit'])->name('edit');
Route::get('show/{user}', [\App\Http\Controllers\Admin\UserController::class, 'show'])->name('show'); Route::get('log/{item}', [\App\Http\Controllers\Admin\UserController::class, 'log'])->name('log');
Route::post('update/{user}', [\App\Http\Controllers\Admin\UserController::class, 'update'])->name('update'); Route::get('show/{item}', [\App\Http\Controllers\Admin\UserController::class, 'show'])->name('show');
Route::get('delete/{user}', [\App\Http\Controllers\Admin\UserController::class, 'destroy'])->name('destroy'); Route::post('update/{item}', [\App\Http\Controllers\Admin\UserController::class, 'update'])->name('update');
Route::get('delete/{item}', [\App\Http\Controllers\Admin\UserController::class, 'destroy'])->name('destroy');
Route::get('restore/{item}', [\App\Http\Controllers\Admin\UserController::class, 'restore'])->name('restore');
Route::post('bulk', [\App\Http\Controllers\Admin\UserController::class, "bulk"])->name('bulk'); Route::post('bulk', [\App\Http\Controllers\Admin\UserController::class, "bulk"])->name('bulk');
Route::get('trashed', [\App\Http\Controllers\Admin\UserController::class, "trashed"])->name('trashed'); Route::get('trashed', [\App\Http\Controllers\Admin\UserController::class, "trashed"])->name('trashed');
}); });

Loading…
Cancel
Save