customer level and customer filter

dev
Aji Kamaludin 1 year ago
parent 8cbabb91f3
commit 25f837bc79
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -15,13 +15,13 @@
# Back Office
- [ ] tambah biaya admin di deposit manual transfer
- [x] tambah biaya admin di deposit manual transfer
- [x] info di ubah jadi html
- [ ] tambahan detail customer untuk detail mitra wbb
- [ ] detail customer level untuk tampilan screen level customer di depan
- [ ] rombak fitur affiliasi
- [ ] tambah detail di user admin
- [ ] tambah logo bank
- [ ] tambah setor tunai
- [x] tambah detail di user admin
- [x] tambah logo bank
- [x] tambah setor tunai
- [ ] pengaturan share dapat menggunakan html
- [ ] menu mitrawbb

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\CustomerLevel;
use Illuminate\Http\Request;
class CustomerLevelController extends Controller
{
public function index(Request $request)
{
return CustomerLevel::all();
}
}

@ -58,10 +58,11 @@ class AuthController extends Controller
->with('message', ['type' => 'error', 'message' => 'Akun belum aktif, Silahkan klik link verifikasi di email anda']);
}
if ($user->status == Customer::STATUS_SUSPEND) {
return redirect()->route('customer.login')
->with('message', ['type' => 'error', 'message' => 'Akun anda telah disuspend, silahkan hubungi penyedia layanan']);
}
// Akun Suspend
// if ($user->status == Customer::STATUS_SUSPEND) {
// return redirect()->route('customer.login')
// ->with('message', ['type' => 'error', 'message' => 'Akun anda telah disuspend, silahkan hubungi penyedia layanan']);
// }
$isAuth = Auth::guard('customer')->login($user);
if ($isAuth) {
@ -111,10 +112,11 @@ class AuthController extends Controller
$customer->update(['google_oauth_response' => json_encode($user)]);
}
if ($customer->status == Customer::STATUS_SUSPEND) {
return redirect()->route('customer.login')
->with('message', ['type' => 'error', 'message' => 'Akun anda telah disuspend, silahkan hubungi penyedia layanan']);
}
// Akun Suspend
// if ($customer->status == Customer::STATUS_SUSPEND) {
// return redirect()->route('customer.login')
// ->with('message', ['type' => 'error', 'message' => 'Akun anda telah disuspend, silahkan hubungi penyedia layanan']);
// }
Auth::guard('customer')->loginUsingId($customer->id);

@ -11,17 +11,41 @@ class CustomerController extends Controller
{
public function index(Request $request)
{
$query = Customer::query()->with(['level'])->orderBy('updated_at', 'desc');
$stats = [
'basic_count' => Customer::whereHas('level', fn ($q) => $q->where('key', CustomerLevel::BASIC))->count(),
'silver_count' => Customer::whereHas('level', fn ($q) => $q->where('key', CustomerLevel::SILVER))->count(),
'gold_count' => Customer::whereHas('level', fn ($q) => $q->where('key', CustomerLevel::GOLD))->count(),
'platinum_count' => Customer::whereHas('level', fn ($q) => $q->where('key', CustomerLevel::PLATINUM))->count(),
];
$query = Customer::query()->with(['level', 'paylater', 'locationFavorites']);
if ($request->q != '') {
$query->where('name', 'like', "%$request->q%")
->orWhere('fullname', 'like', "%$request->q%")
->orWhere('email', 'like', "%$request->q%")
->orWhere('phone', 'like', "%$request->q%");
$query->where(function ($query) use ($request) {
$query->where('name', 'like', "%$request->q%")
->orWhere('fullname', 'like', "%$request->q%")
->orWhere('email', 'like', "%$request->q%")
->orWhere('phone', 'like', "%$request->q%");
});
}
if ($request->location_id != '') {
$query->whereHas('locationFavorites', fn ($q) => $q->where('id', $request->location_id));
}
if ($request->level_id != '') {
$query->where('customer_level_id', $request->level_id);
}
if ($request->sortBy != '' && $request->sortRule != '') {
$query->orderBy($request->sortBy, $request->sortRule);
} else {
$query->orderBy('updated_at', 'desc');
}
return inertia('Customer/Index', [
'query' => $query->paginate(),
'stats' => $stats,
]);
}

@ -16,6 +16,13 @@ class CustomerLevelController extends Controller
]);
}
public function edit(CustomerLevel $customerLevel)
{
return inertia('CustomerLevel/Form', [
'customer_level' => $customerLevel
]);
}
public function update(Request $request, CustomerLevel $customerLevel)
{
$request->validate([
@ -23,15 +30,24 @@ class CustomerLevelController extends Controller
'description' => 'nullable|string',
'min_amount' => 'required|numeric|min:0',
'max_amount' => 'required|numeric|min:0',
'logo' => 'nullable|image',
]);
if ($request->hasFile('logo')) {
$file = $request->file('logo');
$file->store('uploads', 'public');
$customerLevel->logo = $file->hashName('uploads');
}
$customerLevel->update([
'name' => $request->name,
'description' => $request->description,
'min_amount' => $request->min_amount,
'max_amount' => $request->max_amount,
'logo' => $customerLevel->logo,
]);
session()->flash('message', ['type' => 'success', 'message' => 'Item has beed updated']);
return redirect()->route('customer-level.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed updated']);
}
}

@ -61,7 +61,6 @@ class SettingController extends Controller
]);
}
public function updatePayment(Request $request)
{
$request->validate([
@ -90,4 +89,43 @@ class SettingController extends Controller
session()->flash('message', ['type' => 'success', 'message' => 'Setting has beed saved']);
}
public function affilate()
{
$setting = Setting::all();
return inertia('Setting/Payment', [
'setting' => $setting,
'midtrans_notification_url' => route('api.midtrans.notification'),
]);
}
public function updateAffilate(Request $request)
{
$request->validate([
'MIDTRANS_SERVER_KEY' => 'required|string',
'MIDTRANS_CLIENT_KEY' => 'required|string',
'MIDTRANS_MERCHANT_ID' => 'required|string',
'MIDTRANS_ADMIN_FEE' => 'required|numeric',
'MIDTRANS_ENABLED' => 'required|in:0,1',
'midtrans_logo_file' => 'nullable|image',
]);
DB::beginTransaction();
foreach ($request->except(['midtrans_logo_file']) as $key => $value) {
Setting::where('key', $key)->update(['value' => $value]);
}
if ($request->hasFile('midtrans_logo_file')) {
$file = $request->file('midtrans_logo_file');
$file->store('uploads', 'public');
Setting::where('key', 'MIDTRANS_LOGO')->update(['value' => $file->hashName('uploads')]);
}
Cache::flush();
DB::commit();
session()->flash('message', ['type' => 'success', 'message' => 'Setting has beed saved']);
}
}

@ -65,6 +65,7 @@ class Customer extends Authenticatable
'display_deposit',
'display_poin',
'display_phone',
'paylater_remain',
'paylater_limit',
'is_allow_paylater',
'verification_status',
@ -160,7 +161,7 @@ class Customer extends Authenticatable
});
}
public function paylaterLimit(): Attribute
public function paylaterRemain(): Attribute
{
return Attribute::make(get: function () {
if ($this->is_allow_paylater) {
@ -171,6 +172,17 @@ class Customer extends Authenticatable
});
}
public function paylaterLimit(): Attribute
{
return Attribute::make(get: function () {
if ($this->is_allow_paylater) {
return $this->paylater->limit;
}
return '';
});
}
public function isAllowPaylater(): Attribute
{
return Attribute::make(get: function () {

@ -2,6 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
class CustomerLevel extends Model
{
const BASIC = 'basic';
@ -25,11 +27,22 @@ class CustomerLevel extends Model
'name',
'description',
'key',
'logo',
'min_amount',
'max_amount',
'max_loan',
];
protected $appends = [
'logo_url',
];
protected function logoUrl(): Attribute
{
return Attribute::make(get: function () {
return asset($this->logo);
});
}
public static function getByKey($key)
{
return CustomerLevel::where('key', $key)->first();

@ -15,7 +15,8 @@ return new class extends Migration
$table->ulid('id')->primary();
$table->string('name')->nullable();
$table->string('description')->nullable();
$table->string('logo')->nullable();
$table->text('description')->nullable();
$table->string('key')->nullable();
$table->decimal('min_amount', 20, 2)->default(0);
$table->decimal('max_amount', 20, 2)->default(0);

@ -51,10 +51,39 @@ class InstallationSeed extends Seeder
public function customer_levels()
{
$levels = [
['name' => 'Basic', 'key' => 'basic', 'description' => '-', 'min_amount' => '100000', 'max_amount' => '500000'],
['name' => 'Silver', 'key' => 'silver', 'description' => '-', 'min_amount' => '100000', 'max_amount' => '1000000'],
['name' => 'Gold', 'key' => 'gold', 'description' => '-', 'min_amount' => '100000', 'max_amount' => '2000000'],
['name' => 'Platinum', 'key' => 'platinum', 'description' => '-', 'min_amount' => '100000', 'max_amount' => '3000000'],
[
'name' => 'Basic',
'key' => 'basic',
'logo' => 'sample/basic.png',
'description' => '-',
'min_amount' =>
'100000',
'max_amount' => '500000'
],
[
'name' => 'Silver',
'key' => 'silver',
'logo' => 'sample/silver.png',
'description' => '-',
'min_amount' => '100000',
'max_amount' => '1000000'
],
[
'name' => 'Gold',
'key' => 'gold',
'logo' => 'sample/gold.png',
'description' => '-',
'min_amount' => '100000',
'max_amount' => '2000000'
],
[
'name' => 'Platinum',
'key' => 'platinum',
'logo' => 'sample/platinum.png',
'description' => '-',
'min_amount' => '100000',
'max_amount' => '3000000'
],
];
foreach ($levels as $level) {

@ -58,6 +58,8 @@ class PermissionSeeder extends Seeder
['id' => Str::ulid(), 'label' => 'View Customer Verification', 'name' => 'view-customer-verification'],
['id' => Str::ulid(), 'label' => 'View Setting', 'name' => 'view-setting'],
['id' => Str::ulid(), 'label' => 'View Setting Payment Gatewat', 'name' => 'view-setting-payment-gateway'],
['id' => Str::ulid(), 'label' => 'View Setting Affilate', 'name' => 'view-setting-affilate'],
['id' => Str::ulid(), 'label' => 'View Deposit', 'name' => 'view-deposit'],
['id' => Str::ulid(), 'label' => 'Update Deposit', 'name' => 'update-deposit'],

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -10,7 +10,7 @@ export default function BalanceBanner({ user }) {
onClick={() => router.get(route('customer.deposit.index'))}
>
<div className="flex flex-row w-full shadow py-2 px-2 rounded bg-white items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col w-full">
<div className="text-xs flex flex-row items-center space-x-1 text-gray-400">
<HiOutlineCash />
<div>Saldo</div>

@ -8,11 +8,13 @@ import {
HiOutlineCash,
HiOutlineTable,
HiCash,
HiArrowCircleUp,
} from 'react-icons/hi'
import {
HiArchiveBox,
HiBanknotes,
HiCheckBadge,
HiCog8Tooth,
HiCreditCard,
HiCurrencyDollar,
HiMap,
@ -111,15 +113,7 @@ export default [
icon: HiUser,
items: [
{
name: 'Verifikasi',
show: true,
icon: HiCheckBadge,
route: route('customer-verification.index'),
active: 'customer-verification.*',
permission: 'view-customer-verification',
},
{
name: 'Customer',
name: 'List',
show: true,
icon: HiUserCircle,
route: route('customer.index'),
@ -127,13 +121,29 @@ export default [
permission: 'view-customer',
},
{
name: 'Level',
name: 'Atur Level',
show: true,
icon: HiUserCircle,
icon: HiArrowCircleUp,
route: route('customer-level.index'),
active: 'customer-level.*',
permission: 'view-customer-level',
},
{
name: 'Atur Affilate',
show: true,
icon: HiCog8Tooth,
route: route('setting.affilate'),
active: 'setting.affilate',
permission: 'view-setting-affilate',
},
{
name: 'Verifikasi',
show: true,
icon: HiCheckBadge,
route: route('customer-verification.index'),
active: 'customer-verification.*',
permission: 'view-customer-verification',
},
],
},
{
@ -175,10 +185,10 @@ export default [
{
name: 'Payment Gateway',
show: true,
icon: HiOutlineTable,
icon: HiCog8Tooth,
route: route('setting.payment'),
active: 'setting.payment',
permission: 'view-setting',
permission: 'view-setting-payment-gateway',
},
{
name: 'Cash / Setor Tunai',

@ -6,20 +6,33 @@ import { Button, Dropdown } from 'flowbite-react'
import { HiPencil, HiTrash } from 'react-icons/hi'
import { useModalState } from '@/hooks'
import { formatIDR, hasPermission } from '@/utils'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Pagination from '@/Components/Pagination'
import ModalConfirm from '@/Components/ModalConfirm'
import SearchInput from '@/Components/SearchInput'
import { hasPermission } from '@/utils'
import LocationSelectionInput from '../Location/SelectionInput'
import LevelSelectionInput from '../CustomerLevel/SelectionInput'
import ThSort from '@/Components/ThSortComponent'
export default function Customer(props) {
const {
query: { links, data },
stats,
auth,
_search,
_sortBy,
_sortOrder,
} = props
const [search, setSearch] = useState('')
const preValue = usePrevious(search)
const [location, setLocation] = useState(null)
const [level, setLevel] = useState(null)
const [search, setSearch] = useState({
q: _search,
sortBy: _sortBy,
sortOrder: _sortOrder,
})
const preValue = usePrevious(`${search}${location}${level}`)
const confirmModal = useModalState()
@ -34,19 +47,42 @@ export default function Customer(props) {
}
}
const params = { q: search }
const handleSearchChange = (e) => {
setSearch({
...search,
q: e.target.value,
})
}
const sort = (key, sort = null) => {
if (sort !== null) {
setSearch({
...search,
sortBy: key,
sortRule: sort,
})
return
}
setSearch({
...search,
sortBy: key,
sortRule: search.sortRule == 'asc' ? 'desc' : 'asc',
})
}
const params = { q: search, location_id: location, level_id: level }
useEffect(() => {
if (preValue) {
router.get(
route(route().current()),
{ q: search },
{ ...search, location_id: location, level_id: level },
{
replace: true,
preserveState: true,
}
)
}
}, [search])
}, [search, location, level])
const canCreate = hasPermission(auth, 'create-customer')
const canUpdate = hasPermission(auth, 'update-customer')
@ -56,8 +92,36 @@ export default function Customer(props) {
<AuthenticatedLayout page={'Customer'} action={''}>
<Head title="Customer" />
<div>
<div className="mx-auto sm:px-6 lg:px-8 ">
<div className="w-full lg:max-w-[1100px] 2xl:max-w-full">
<div className="mx-auto sm:px-6 lg:px-8">
<div className="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 mb-2">
<div className="border rounded-md shadow bg-white px-4 py-2 flex flex-col">
<div className="text-gray-600 text-lg">Basic</div>
<div className="font-bold text-xl pt-2">
{formatIDR(stats.basic_count)} Orang
</div>
</div>
<div className="border rounded-md shadow bg-white px-4 py-2 flex flex-col">
<div className="text-gray-600 text-lg">Silver</div>
<div className="font-bold text-xl pt-2">
{formatIDR(stats.silver_count)} Orang
</div>
</div>
<div className="border rounded-md shadow bg-white px-4 py-2 flex flex-col">
<div className="text-gray-600 text-lg">Gold</div>
<div className="font-bold text-xl pt-2">
{formatIDR(stats.gold_count)} Orang
</div>
</div>
<div className="border rounded-md shadow bg-white px-4 py-2 flex flex-col">
<div className="text-gray-600 text-lg">
Platinum
</div>
<div className="font-bold text-xl pt-2">
{formatIDR(stats.platinum_count)} Orang
</div>
</div>
</div>
<div className="p-6 overflow-hidden shadow-sm sm:rounded-lg bg-gray-200 dark:bg-gray-800 space-y-4">
<div className="flex justify-between">
{canCreate && (
@ -65,14 +129,36 @@ export default function Customer(props) {
<Button size="sm">Tambah</Button>
</Link>
)}
<div className="flex items-center">
<SearchInput
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<div className="flex flex-col gap-1 items-end">
<div className="max-w-md">
<SearchInput
onChange={handleSearchChange}
value={search.q}
/>
</div>
<div className="flex flex-row gap-1">
<div className="w-full max-w-md">
<LevelSelectionInput
itemSelected={level}
onItemSelected={(id) =>
setLevel(id)
}
placeholder={'filter level'}
/>
</div>
<div className="w-full max-w-md">
<LocationSelectionInput
itemSelected={location}
onItemSelected={(id) =>
setLocation(id)
}
placeholder={'filter lokasi'}
/>
</div>
</div>
</div>
</div>
<div className="overflow-auto">
<div className="w-full overflow-auto">
<div>
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400 mb-4">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
@ -89,17 +175,31 @@ export default function Customer(props) {
>
Level
</th>
<ThSort
sort={sort}
label={'deposit_balance'}
search={search}
>
Deposit
</ThSort>
<ThSort
sort={sort}
label={'poin_balance'}
search={search}
>
Poin
</ThSort>
<th
scope="col"
className="py-3 px-6"
>
Deposit
Sisa Saldo Hutang
</th>
<th
scope="col"
className="py-3 px-6"
>
poin
Limit Hutang
</th>
<th
scope="col"
@ -107,6 +207,18 @@ export default function Customer(props) {
>
Referral Code
</th>
<th
scope="col"
className="py-3 px-6"
>
Lokasi
</th>
<th
scope="col"
className="py-3 px-6"
>
Whatsapp
</th>
<th
scope="col"
className="py-3 px-6"
@ -127,9 +239,16 @@ export default function Customer(props) {
>
<td
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white hover:underline"
>
{customer.name}
<Link
href={route(
'customer.edit',
customer
)}
>
{customer.name}
</Link>
</td>
<td
scope="row"
@ -149,12 +268,59 @@ export default function Customer(props) {
>
{customer.display_poin}
</td>
<td
scope="row"
className="py-4 px-6"
>
{formatIDR(
customer.paylater_remain
)}
</td>
<td
scope="row"
className="py-4 px-6"
>
{formatIDR(
customer.paylater_limit
)}
</td>
<td
scope="row"
className="py-4 px-6"
>
{customer.referral_code}
</td>
<td
scope="row"
className="py-4 px-6"
>
{customer.location_favorites.map(
(location) => (
<div
key={
location.id
}
>
{location.name}
</div>
)
)}
</td>
<td
scope="row"
className="py-4 px-6"
>
{customer.phone !==
null && (
<a
href={`https://wa.me/+62${customer.phone}`}
target="_blank"
className="underline"
>
+62{customer.phone}
</a>
)}
</td>
<td
scope="row"
className="py-4 px-6"

@ -0,0 +1,121 @@
import React, { useEffect, Suspense } from 'react'
import { isEmpty } from 'lodash'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import FormInput from '@/Components/FormInput'
import Button from '@/Components/Button'
import { Head, useForm } from '@inertiajs/react'
import FormFile from '@/Components/FormFile'
const TinyEditor = React.lazy(() => import('@/Components/TinyMCE'))
export default function Form(props) {
const { customer_level } = props
const { data, setData, post, processing, errors } = useForm({
name: '',
description: '',
min_amount: 0,
max_amount: 0,
logo: null,
logo_url: '',
})
const handleOnChange = (event) => {
setData(
event.target.name,
event.target.type === 'checkbox'
? event.target.checked
? 1
: 0
: event.target.value
)
}
const handleSubmit = () => {
post(route('customer-level.update', customer_level.id))
}
useEffect(() => {
if (isEmpty(customer_level) === false) {
setData({
name: customer_level.name,
description: customer_level.description,
min_amount: customer_level.min_amount,
max_amount: customer_level.max_amount,
logo_url: customer_level.logo_url,
})
}
}, [customer_level])
return (
<AuthenticatedLayout
page={'Atur Level'}
action={'Form'}
parent={route('customer-level.index')}
>
<Head title="Atur Level" />
<div>
<div className="mx-auto sm:px-6 lg:px-8">
<div className="overflow-hidden p-4 shadow-sm sm:rounded-lg bg-white dark:bg-gray-800 flex flex-col ">
<div className="text-xl font-bold mb-4">Atur Level</div>
<FormInput
name="name"
value={data.name}
onChange={handleOnChange}
label="Nama"
error={errors.name}
/>
<FormFile
label={'Logo'}
onChange={(e) => setData('logo', e.target.files[0])}
error={errors.logo}
preview={
isEmpty(data.logo_url) === false && (
<img
src={data.logo_url}
className="mb-1 h-24 object-cover"
alt="preview"
/>
)
}
/>
<div className="py-4">
<Suspense fallback={<div>Loading...</div>}>
<TinyEditor
value={data.description}
init={{
height: 500,
// menubar: false,
menubar:
'file edit view insert format tools table help',
plugins:
'preview importcss searchreplace autolink directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap emoticons',
toolbar_mode: 'scrolling',
toolbar:
'undo redo | insertfile image media link | bold italic underline strikethrough | fontfamily fontsize blocks | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | charmap emoticons | fullscreen preview save print | ltr rtl | anchor codesample',
}}
onEditorChange={(newValue, editor) => {
setData(
'description',
editor.getContent()
)
}}
/>
</Suspense>
</div>
<div className="mt-8">
<Button
onClick={handleSubmit}
processing={processing}
>
Simpan
</Button>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
)
}

@ -1,104 +0,0 @@
import React, { useEffect } from 'react'
import Modal from '@/Components/Modal'
import { useForm } from '@inertiajs/react'
import Button from '@/Components/Button'
import FormInput from '@/Components/FormInput'
import RoleSelectionInput from '../Role/SelectionInput'
import { isEmpty } from 'lodash'
export default function FormModal(props) {
const { modalState } = props
const { data, setData, post, put, processing, errors, reset, clearErrors } =
useForm({
name: '',
description: '',
min_amount: 0,
max_amount: 0,
})
const handleOnChange = (event) => {
setData(
event.target.name,
event.target.type === 'checkbox'
? event.target.checked
? 1
: 0
: event.target.value
)
}
const handleReset = () => {
modalState.setData(null)
reset()
clearErrors()
}
const handleClose = () => {
handleReset()
modalState.toggle()
}
const handleSubmit = () => {
const customerLevel = modalState.data
put(route('customer-level.update', customerLevel.id), {
onSuccess: () => handleClose(),
})
}
useEffect(() => {
const customerLevel = modalState.data
if (isEmpty(customerLevel) === false) {
setData({
name: customerLevel.name,
description: customerLevel.description,
min_amount: customerLevel.min_amount,
max_amount: customerLevel.max_amount,
})
return
}
}, [modalState])
return (
<Modal isOpen={modalState.isOpen} toggle={handleClose} title={'Info'}>
<FormInput
name="name"
value={data.name}
onChange={handleOnChange}
label="Nama"
error={errors.name}
/>
<FormInput
name="description"
value={data.description}
onChange={handleOnChange}
label="Description"
error={errors.description}
/>
<FormInput
type="number"
name="min_amount"
value={data.min_amount}
onChange={handleOnChange}
label="Minimal Deposit"
error={errors.min_amount}
/>
<FormInput
type="number"
name="max_amount"
value={data.max_amount}
onChange={handleOnChange}
label="Maksimal Deposit"
error={errors.max_amount}
/>
<div className="flex items-center">
<Button onClick={handleSubmit} processing={processing}>
Simpan
</Button>
<Button onClick={handleClose} type="secondary">
Batal
</Button>
</div>
</Modal>
)
}

@ -1,13 +1,11 @@
import React from 'react'
import { Head } from '@inertiajs/react'
import { Dropdown } from 'flowbite-react'
import { Head, Link } from '@inertiajs/react'
import { HiPencil } from 'react-icons/hi'
import { useModalState } from '@/hooks'
import { formatIDR, hasPermission } from '@/utils'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Pagination from '@/Components/Pagination'
import FormModal from './FormModal'
import { formatIDR, hasPermission } from '@/utils'
export default function Info(props) {
const {
@ -17,11 +15,6 @@ export default function Info(props) {
const formModal = useModalState()
const toggleFormModal = (customerlevel = null) => {
formModal.setData(customerlevel)
formModal.toggle()
}
const canUpdate = hasPermission(auth, 'update-customer-level')
return (
@ -41,25 +34,13 @@ export default function Info(props) {
scope="col"
className="py-3 px-6"
>
Name
</th>
<th
scope="col"
className="py-3 px-6"
>
Description
Logo
</th>
<th
scope="col"
className="py-3 px-6"
>
Minimal Deposit
</th>
<th
scope="col"
className="py-3 px-6"
>
Maximal Deposit
Name
</th>
<th
scope="col"
@ -77,34 +58,27 @@ export default function Info(props) {
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{level.name}
</td>
<td className="py-4 px-6">
{level.description}
<img
src={level.logo_url}
className="h-20"
alt="logo alt"
/>
</td>
<td className="py-4 px-6">
{formatIDR(
level.min_amount
)}
</td>
<td className="py-4 px-6">
{formatIDR(
level.max_amount
)}
{level.name}
</td>
<td className="py-4 px-6 flex justify-center">
<td className="py-4 px-6 flex flex-row justify-end items-center">
{canUpdate && (
<div
<Link
className="flex space-x-1 items-center hover:underline"
onClick={() =>
toggleFormModal(
level
)
}
href={route(
'customer-level.edit',
level.id
)}
>
<HiPencil />
<div>Ubah</div>
</div>
</Link>
)}
</td>
</tr>
@ -119,7 +93,6 @@ export default function Info(props) {
</div>
</div>
</div>
<FormModal modalState={formModal} />
</AuthenticatedLayout>
)
}

@ -0,0 +1,263 @@
import React, { useRef, useEffect, useState } from 'react'
import { useDebounce } from '@/hooks'
import { usePage } from '@inertiajs/react'
import axios from 'axios'
import { HiChevronDown, HiChevronUp, HiX } from 'react-icons/hi'
import { Spinner } from 'flowbite-react'
export default function SelectionInput(props) {
const ref = useRef()
const {
props: { auth },
} = usePage()
const {
label = '',
itemSelected = null,
onItemSelected = () => {},
disabled = false,
placeholder = '',
error = '',
all = 0,
} = props
const [showItems, setShowItem] = useState([])
const [isSelected, setIsSelected] = useState(true)
const [selected, setSelected] = useState(null)
const [query, setQuery] = useState('')
const q = useDebounce(query, 300)
const [isOpen, setIsOpen] = useState(false)
const [loading, setLoading] = useState(false)
const toggle = () => {
setQuery('')
setIsOpen(!isOpen)
}
const onInputMouseDown = () => {
setIsSelected(false)
setQuery('')
setIsOpen(!isOpen)
}
const handleSelectItem = (item) => {
setIsSelected(true)
onItemSelected(item.id)
setSelected(item.name)
setIsOpen(false)
}
const removeItem = () => {
setIsSelected(false)
setSelected('')
onItemSelected(null)
}
const filterItems = (value) => {
setIsSelected(false)
setQuery(value)
}
useEffect(() => {
if (isOpen === true) {
const checkIfClickedOutside = (e) => {
if (isOpen && ref.current && !ref.current.contains(e.target)) {
setIsOpen(false)
if (selected !== null) {
setIsSelected(true)
}
}
}
document.addEventListener('mousedown', checkIfClickedOutside)
return () => {
document.removeEventListener('mousedown', checkIfClickedOutside)
}
}
}, [isOpen])
const fetch = (q = '') => {
setLoading(true)
axios
.get(route('api.customer-level.index', { q: q, all: all }), {
headers: {
'Content-Type': 'application/json',
// 'Authorization': 'Bearer ' + auth.user.jwt_token
},
})
.then((response) => {
setShowItem(response.data)
})
.catch((err) => {
alert(err)
})
.finally(() => setLoading(false))
}
// every select item open
useEffect(() => {
if (isOpen) {
fetch(q)
}
}, [q, isOpen])
// once page load
useEffect(() => {
fetch()
}, [])
useEffect(() => {
if (disabled) {
setSelected('')
}
}, [disabled])
useEffect(() => {
if (itemSelected !== null) {
const item = showItems.find((item) => item.id === itemSelected)
if (item) {
setSelected(item.name)
setIsSelected(true)
}
return
}
setIsSelected(false)
}, [itemSelected, loading])
useEffect(() => {
if (isSelected && selected === '') {
setSelected('')
setIsSelected(false)
}
}, [isSelected])
return (
<div className="flex flex-col items-center" ref={ref}>
<div className="w-full flex flex-col items-center">
<div className="w-full">
<div className="flex flex-col relative">
{label !== '' && (
<label
htmlFor="first_name"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
{label}
</label>
)}
<div className="w-full">
<div
className={`p-1.5 bg-gray-50 dark:bg-gray-700 flex border rounded-lg
${
error
? 'border-red-500'
: 'border-gray-300 dark:border-gray-600'
}
${disabled ? 'bg-gray-50' : ''}`}
>
<input
className="block w-full text-sm bg-gray-50 text-gray-900 dark:border-gray-700 border cursor-pointer dark:text-gray-300 outline-none border-transparent dark:bg-gray-700 dark:placeholder-gray-400 px-1"
onMouseDown={onInputMouseDown}
placeholder={placeholder}
value={`${
isSelected
? selected === null
? ''
: selected
: query
}`}
onChange={(e) =>
filterItems(e.target.value)
}
disabled={disabled}
/>
{isSelected && (
<div
onClick={
disabled ? () => {} : removeItem
}
>
<button className="cursor-pointer w-6 h-6 text-red-300 outline-none focus:outline-none">
<HiX />
</button>
</div>
)}
<div onClick={disabled ? () => {} : toggle}>
<button className="cursor-pointer w-6 h-6 text-gray-300 outline-none focus:outline-none">
{isOpen ? (
<HiChevronUp />
) : (
<HiChevronDown />
)}
</button>
</div>
</div>
{error && (
<p className="mb-2 text-sm text-red-600 dark:text-red-500">
{error}
</p>
)}
</div>
{isOpen && (
<div
className="absolute mt-1 shadow-lg bg-gray-50 dark:bg-gray-700 dark:text-gray-200 top-100 z-40 w-full lef-0 rounded overflow-y-auto"
style={{ maxHeight: '300px', top: '100%' }}
>
<div className="flex flex-col w-full">
{loading ? (
<div>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content">
<div className="w-full items-center justify-center flex">
<div className="mx-2 my-5">
<Spinner className="mr-2" />
<span>Loading...</span>
</div>
</div>
</div>
</div>
) : (
<>
{showItems.map((item, index) => (
<div
key={index}
onClick={() =>
handleSelectItem(item)
}
>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content hover:bg-gray-400 bg-opacity-10 dark:hover:bg-gray-200 dark:hover:bg-opacity-10 dark:hover:text-gray-100">
<div className="w-full items-center flex">
<div className="mx-2">
<span>
{item.name}
</span>
</div>
</div>
</div>
</div>
))}
{showItems.length <= 0 && (
<div>
<div className="flex w-full items-center p-2 pl-2 border-transparent border-l-2 relative hover:border-neutral-content">
<div className="w-full items-center justify-center flex">
<div className="mx-2 my-5">
<span>
No Items
Found
</span>
</div>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}

@ -116,7 +116,7 @@ export default function Product(props) {
key={
user.id
}
className="px-2 py-1 bg-blue-600 text-white border rounded-full border-b-blue-900"
className="px-2 py-1 bg-blue-600 text-white border rounded-full border-blue-900"
onClick={() =>
router.visit(
route(

@ -11,7 +11,7 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Pagination from '@/Components/Pagination'
import ModalConfirm from '@/Components/ModalConfirm'
import SearchInput from '@/Components/SearchInput'
import ThSort from './ThSortComponent'
import ThSort from '@/Components/ThSortComponent'
import ModalFilter from './ModalFilter'
import ModalDelete from './ModalDelete'

@ -101,7 +101,8 @@ Route::middleware(['http_secure_aware', 'inertia.admin'])
// customer level
Route::get('/customer-levels', [CustomerLevelController::class, 'index'])->name('customer-level.index');
Route::put('/customer-levels/{customerLevel}', [CustomerLevelController::class, 'update'])->name('customer-level.update');
Route::get('/customer-levels/{customerLevel}', [CustomerLevelController::class, 'edit'])->name('customer-level.edit');
Route::post('/customer-levels/{customerLevel}', [CustomerLevelController::class, 'update'])->name('customer-level.update');
// verification
Route::get('/customers-verifications', [VerificationController::class, 'index'])->name('customer-verification.index');
@ -133,6 +134,8 @@ Route::middleware(['http_secure_aware', 'inertia.admin'])
// setting
Route::get('/payment-gateway', [SettingController::class, 'payment'])->name('setting.payment');
Route::post('/payment-gateway', [SettingController::class, 'updatePayment']);
Route::get('/affilate', [SettingController::class, 'affilate'])->name('setting.affilate');
Route::post('/affilate', [SettingController::class, 'updateAffilate']);
Route::get('/settings', [SettingController::class, 'index'])->name('setting.index');
Route::post('/settings', [SettingController::class, 'update'])->name('setting.update');

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Api\CustomerController;
use App\Http\Controllers\Api\CustomerLevelController;
use App\Http\Controllers\Api\LocationController;
use App\Http\Controllers\Api\LocationProfileController;
use App\Http\Controllers\Api\NotificationController;
@ -29,6 +30,7 @@ Route::get('/roles', [RoleController::class, 'index'])->name('api.role.index');
Route::get('/locations', [LocationController::class, 'index'])->name('api.location.index');
Route::get('/location-profiles', [LocationProfileController::class, 'index'])->name('api.location-profile.index');
Route::get('/customers', [CustomerController::class, 'index'])->name('api.customer.index');
Route::get('/customer-levels', [CustomerLevelController::class, 'index'])->name('api.customer-level.index');
Route::get('/notifications/{notif?}', [NotificationController::class, 'update'])->name('api.notification.update');
// midtrans

Loading…
Cancel
Save