verified customer

dev
Aji Kamaludin 1 year ago
parent 1c76943f3e
commit 5220417caa
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -13,12 +13,12 @@
- [x] Setting Level Customer (view levels, edit name of level,minimal saldo, max amount saldo, max hutang)
- [x] Deposit Menu (view daftar histori deposit)
- [x] Manual Approve Deposit
- [ ] List Customer Verification
- [ ] Manual Approve Verification -> mendapatkan limit hutang
- [x] List Customer Verification
- [x] Manual Approve Verification -> mendapatkan limit hutang
- [ ] Setting Bonus Coin (memasukan amount bonus coin yang didapat dengan level dan harga voucher) - bonus coin
- [ ] Voucher Sales
- [ ] Dashboard (gafik hasil penjualan : disorting tanggal, lokasi dan customer)
- [ ] Notification (manual deposit, stock voucher)
- [ ] Notification (manual deposit, deposit success, stock voucher, sale)
- [ ] View Customer Coin History
- [ ] Voucher - harga per level
- [ ] Voucher - harga coin
@ -42,7 +42,7 @@
- [x] Customer Share Buyed Voucher, via WA dll
- [x] Register Refferal
- [x] Customer View Coin History
- [ ] Verified Akun
- [ ] Paylater
- [x] Verified Akun
- [ ] Paylater: index paylater, payment cart, deposite repay
- [ ] Coin Explorer
- [ ] Notification (purchase success, deposit success)

@ -138,9 +138,9 @@ class AuthController extends Controller
public function destroy()
{
Auth::logout();
session()->remove('carts');
session()->flush();
Auth::logout();
return redirect()->route('customer.login')
->with('message', ['type' => 'success', 'message' => 'you are logged out, see you next time']);

@ -29,6 +29,8 @@ class CartController extends Controller
return inertia('Cart/Index', [
'carts' => $carts,
'total' => $total,
'allow_process' => '', //TODO: check allow process payment
'is_paylater' => '', //check is transaction use paylater
]);
}
@ -138,6 +140,8 @@ class CartController extends Controller
}
}
// TODO: is use paylater track the paylater
// TODO: if use paylater credit all deposit if available
$deposit = $customer->deposites()->create([
'credit' => $total,
'description' => 'Pembayaran #' . $sale->code,

@ -106,6 +106,7 @@ class DepositController extends Controller
if ($transaction_status == 'settlement' || $transaction_status == 'capture') {
$is_valid = DepositHistory::STATUS_VALID;
$deposit->update_customer_balance();
// TODO: add paylater check
} elseif ($transaction_status == 'pending') {
$is_valid = DepositHistory::STATUS_WAIT_PAYMENT;
} else {
@ -136,6 +137,7 @@ class DepositController extends Controller
if ($request->transaction_status == 'settlement' || $request->transaction_status == 'capture') {
$deposit->fill(['payment_status' => DepositHistory::STATUS_VALID]);
$deposit->update_customer_balance();
// TODO: add paylater check
} elseif ($request->transaction_status == 'pending') {
$deposit->fill(['payment_status' => DepositHistory::STATUS_WAIT_PAYMENT]);
} else {

@ -3,13 +3,14 @@
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\CustomerLevel;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function index(Request $request)
{
$query = Customer::query()->with(['level']);
$query = Customer::query()->with(['level'])->orderBy('updated_at', 'desc');
if ($request->q != '') {
$query->where('name', 'like', "%$request->q%")
@ -57,14 +58,15 @@ class CustomerController extends Controller
public function edit(Customer $customer)
{
return inertia('Customer/Form', [
'customer' => $customer,
'customer' => $customer->load(['level']),
'levels' => CustomerLevel::all()
]);
}
public function update(Request $request, Customer $customer)
{
$request->validate([
'username' => 'required|string|min:5|alpha_dash|unique:customers,username,'.$customer->id,
'username' => 'required|string|min:5|alpha_dash|unique:customers,username,' . $customer->id,
'password' => 'nullable|string|min:8',
'name' => 'required|string',
'fullname' => 'required|string',
@ -104,4 +106,25 @@ class CustomerController extends Controller
return redirect()->route('customer.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed deleted']);
}
public function update_level(Request $request, Customer $customer)
{
$request->validate([
'level' => 'required|exists:customer_levels,key',
'paylater_limit' => 'required|numeric',
]);
$level = CustomerLevel::where('key', $request->level)->first();
$customer->update(['customer_level_id' => $level->id]);
$customer->paylater()->updateOrCreate([
'customer_id' => $customer->id,
], [
'limit' => $request->paylater_limit
]);
return redirect()->route('customer.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed updated']);
}
}

@ -55,6 +55,7 @@ class DepositController extends Controller
]);
if ($request->status == DepositHistory::STATUS_VALID) {
$deposit->update_customer_balance();
// TODO: add paylater check
}
DB::commit();

@ -21,6 +21,7 @@ class SettingController extends Controller
public function update(Request $request)
{
$request->validate([
'VOUCHER_STOCK_NOTIFICATION' => 'required|numeric',
'AFFILATE_ENABLED' => 'required|in:0,1',
'AFFILATE_COIN_AMOUNT' => 'required|numeric',
'MIDTRANS_SERVER_KEY' => 'required|string',

@ -34,9 +34,25 @@ class VerificationController extends Controller
public function update(Request $request, Customer $customer)
{
// TODO: here
$request->validate([]);
//
$customer->update([]);
$request->validate([
'level' => 'required|exists:customer_levels,key',
'paylater_limit' => 'required|numeric',
]);
$level = CustomerLevel::where('key', $request->level)->first();
$customer->update([
'customer_level_id' => $level->id,
'identity_verified' => Customer::VERIFIED
]);
$customer->paylater()->updateOrCreate([
'customer_id' => $customer->id,
], [
'limit' => $request->paylater_limit
]);
return redirect()->route('customer-verification.index')
->with('message', ['type' => 'success', 'message' => 'Item has beed updated']);
}
}

@ -51,6 +51,7 @@ class Customer extends Authenticatable
'display_coin',
'display_phone',
'paylater_limit',
'is_allow_paylater'
];
protected static function booted(): void
@ -145,7 +146,17 @@ class Customer extends Authenticatable
public function paylaterLimit(): Attribute
{
return Attribute::make(get: function () {
return $this->paylater?->limit ?? '';
if ($this->is_allow_paylater) {
return $this->paylater->limit;
}
return '';
});
}
public function isAllowPaylater(): Attribute
{
return Attribute::make(get: function () {
return [CustomerLevel::GOLD => true, CustomerLevel::PLATINUM => true][$this->level->key] ?? false;
});
}

@ -28,6 +28,8 @@ class InstallationSeed extends Seeder
['key' => 'MIDTRANS_MERCHANT_ID', 'value' => 'G561244367', 'type' => 'text'],
['key' => 'MIDTRANS_LOGO', 'value' => 'sample/midtrans_logo.png', 'type' => 'image'],
['key' => 'MIDTRANS_ENABLED', 'value' => '0', 'type' => 'text'],
['key' => 'VOUCHER_STOCK_NOTIFICATION', 'value' => '10', 'type' => 'text'],
];
foreach ($settings as $setting) {

@ -7,7 +7,7 @@ import CustomerLayout from '@/Layouts/CustomerLayout'
export default function Detail({ coin }) {
return (
<CustomerLayout>
<Head title="Top Up" />
<Head title="Coin" />
<div className="flex flex-col min-h-[calc(95dvh)]">
<div
className="w-full px-5 py-5"

@ -25,7 +25,7 @@ export default function Index({
return (
<CustomerLayout>
<Head title="Top Up" />
<Head title="Coin" />
<div className="flex flex-col w-full min-h-[calc(90dvh)]">
<div className="w-full pt-10 px-5">
<div className="text-base">{user.fullname}</div>

@ -1,3 +1,4 @@
import { formatIDR } from '@/utils'
import { router } from '@inertiajs/react'
import { HiOutlineCash } from 'react-icons/hi'
@ -25,7 +26,7 @@ export default function BalanceBanner({ user }) {
</div>
<div className="font-bold">{user.level.name} Member</div>
<div className="text-xs flex flex-row items-center space-x-1 text-gray-400">
<div>{user.paylater_limit}</div>
<div>{formatIDR(user.paylater_limit)}</div>
</div>
</div>
</div>

@ -20,7 +20,7 @@ export default function UserBanner({ user }) {
<HiOutlineBell className="text-white w-7 h-7" />
<div>
<div className="bg-white text-blue-700 rounded-lg px-1 text-xs -ml-2.5">
1
0
</div>
</div>
</div>

@ -84,11 +84,12 @@ export default function Index({ auth: { user } }) {
</div>
</div>
<div className="p-4 flex flex-col">
{/* menu hanya muncul untuk member gold atau platinum */}
<div className="flex flex-row justify-between items-center px-2 py-4 w-full border-b border-gray-400 hover:bg-gray-100">
<div>Paylater</div>
<HiChevronRight className="h-5 w-5" />
</div>
{user.is_allow_paylater && (
<div className="flex flex-row justify-between items-center px-2 py-4 w-full border-b border-gray-400 hover:bg-gray-100">
<div>Paylater</div>
<HiChevronRight className="h-5 w-5" />
</div>
)}
<div
className="flex flex-row justify-between items-center px-2 py-4 w-full border-b border-gray-400 hover:bg-gray-100"
onClick={() =>

@ -43,7 +43,7 @@ export default function CustomerLayout({ children }) {
<div className="flex flex-col w-full bg-white shadow pb-20 min-h-[calc(90dvh)] max-w-md">
<div>{children}</div>
</div>
<div className="fixed bottom-0 flex flex-row justify-around w-full bg-gray-50 max-w-md">
<div className="fixed bottom-0 flex flex-row justify-between w-full bg-gray-50 max-w-md">
<div
className={`pb-1 pt-2 px-5 hover:bg-blue-200 flex flex-col items-center ${isActive(
'home.index'

@ -4,13 +4,15 @@ 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 { Head, useForm, usePage } from '@inertiajs/react'
import FormFile from '@/Components/FormFile'
import FormInputWith from '@/Components/FormInputWith'
import TextArea from '@/Components/TextArea'
export default function Form(props) {
const { customer } = props
const Profile = () => {
const {
props: { customer },
} = usePage()
const { data, setData, post, processing, errors } = useForm({
username: '',
@ -56,6 +58,167 @@ export default function Form(props) {
}
}, [customer])
return (
<div className="flex flex-col p-4 border rounded border-gray-200">
<div className="pb-4 font-bold">Profile</div>
<FormInput
name="fullname"
value={data.fullname}
onChange={handleOnChange}
label="Nama Lengkap"
error={errors.fullname}
/>
<FormInput
name="name"
value={data.name}
onChange={handleOnChange}
label="Nama Panggilan"
error={errors.name}
/>
<FormInputWith
type="number"
leftItem={<div className="text-sm">+62</div>}
name="phone"
value={data.phone}
onChange={handleOnChange}
error={errors.phone}
formClassName={'pl-10'}
label="Whatsapp"
/>
<TextArea
name="address"
value={data.address}
onChange={handleOnChange}
label="Alamat Lengkap"
error={errors.address}
rows={4}
/>
<FormInput
name="username"
value={data.username}
onChange={handleOnChange}
label="username"
error={errors.username}
/>
<FormInput
name="password"
value={data.password}
onChange={handleOnChange}
label="password"
error={errors.password}
/>
<FormFile
label={'Image'}
onChange={(e) => setData('image', e.target.files[0])}
error={errors.image}
preview={
isEmpty(data.image_url) === false && (
<img
src={data.image_url}
className="mb-1 h-24 w-24 object-cover rounded-full"
alt="preview"
/>
)
}
/>
<div className="mt-1">
<Button onClick={handleSubmit} processing={processing}>
Simpan
</Button>
</div>
</div>
)
}
const Identity = () => {
const {
props: { customer },
} = usePage()
if (isEmpty(customer.identity_image)) {
return
}
return (
<div className="mt-2 flex flex-col p-4 border rounded border-gray-200">
<div className="pb-4 font-bold">KTP</div>
<img
alt="identity"
src={customer.identity_image_url}
className="w-full object-fill h-96"
/>
</div>
)
}
const Paylater = () => {
const {
props: { customer, levels },
} = usePage()
const { data, setData, post, processing, errors } = useForm({
level: customer.level.key,
paylater_limit: +customer.paylater_limit,
})
const handleOnChange = (event) => {
setData(
event.target.name,
event.target.type === 'checkbox'
? event.target.checked
? 1
: 0
: event.target.value
)
}
const handleSubmit = () => {
post(route('customer.update_level', customer))
}
return (
<div className="mt-2 flex flex-col p-4 border rounded border-gray-200">
<div className="mt-4">
<div className="mb-1 text-sm">Level</div>
<select
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
onChange={handleOnChange}
value={data.level}
name="level"
>
<option value=""></option>
{levels.map((level) => (
<option value={level.key} key={level.key}>
{level.name}
</option>
))}
</select>
{errors.level && (
<p className="mb-2 text-sm text-red-600 dark:text-red-500">
{errors.level}
</p>
)}
</div>
<div className="mb-4 mt-2">
<FormInput
type="number"
label="Limit Hutang"
name="paylater_limit"
onChange={handleOnChange}
value={data.paylater_limit}
error={errors.paylater_limit}
/>
</div>
<div className="flex items-center">
<Button onClick={handleSubmit} processing={processing}>
Simpan
</Button>
</div>
</div>
)
}
export default function Form(props) {
return (
<AuthenticatedLayout
auth={props.auth}
@ -70,77 +233,9 @@ export default function Form(props) {
<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">Customer</div>
<FormInput
name="fullname"
value={data.fullname}
onChange={handleOnChange}
label="Nama Lengkap"
error={errors.fullname}
/>
<FormInput
name="name"
value={data.name}
onChange={handleOnChange}
label="Nama Panggilan"
error={errors.name}
/>
<FormInputWith
type="number"
leftItem={<div className="text-sm">+62</div>}
name="phone"
value={data.phone}
onChange={handleOnChange}
error={errors.phone}
formClassName={'pl-10'}
label="Whatsapp"
/>
<TextArea
name="address"
value={data.address}
onChange={handleOnChange}
label="Alamat Lengkap"
error={errors.address}
rows={4}
/>
<FormInput
name="username"
value={data.username}
onChange={handleOnChange}
label="username"
error={errors.username}
/>
<FormInput
name="password"
value={data.password}
onChange={handleOnChange}
label="password"
error={errors.password}
/>
<FormFile
label={'Image'}
onChange={(e) =>
setData('image', e.target.files[0])
}
error={errors.image}
preview={
isEmpty(data.image_url) === false && (
<img
src={data.image_url}
className="mb-1 h-24 w-24 object-cover rounded-full"
alt="preview"
/>
)
}
/>
<div className="mt-8">
<Button
onClick={handleSubmit}
processing={processing}
>
Simpan
</Button>
</div>
<Profile />
<Identity />
<Paylater />
</div>
</div>
</div>

@ -12,6 +12,10 @@ import { extractValue } from './utils'
export default function General(props) {
const { setting, midtrans_notification_url } = props
const { data, setData, post, reset, processing, errors } = useForm({
VOUCHER_STOCK_NOTIFICATION: extractValue(
setting,
'VOUCHER_STOCK_NOTIFICATION'
),
AFFILATE_ENABLED: extractValue(setting, 'AFFILATE_ENABLED'),
AFFILATE_COIN_AMOUNT: extractValue(setting, 'AFFILATE_COIN_AMOUNT'),
MIDTRANS_SERVER_KEY: extractValue(setting, 'MIDTRANS_SERVER_KEY'),
@ -56,10 +60,20 @@ export default function General(props) {
<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">Setting</div>
<div className="p-2 border rounded-xl">
<div className="font-bold mb-2">Affilate</div>
<div className="font-bold mb-2">Notification</div>
<FormInput
type={'number'}
name="VOUCHER_STOCK_NOTIFICATION"
value={data.VOUCHER_STOCK_NOTIFICATION}
onChange={handleOnChange}
label="Jumlah Stok"
error={errors.VOUCHER_STOCK_NOTIFICATION}
/>
</div>
<div className="mt-2 p-2 border rounded-xl">
<div className="font-bold mb-2">Affilate</div>
<FormInput
type={'number'}
name="AFFILATE_COIN_AMOUNT"
@ -75,6 +89,7 @@ export default function General(props) {
name="AFFILATE_ENABLED"
/>
</div>
<div className="mt-2 p-2 border rounded-xl">
<div className="font-bold mb-2">
Midtrans Payment

@ -4,13 +4,14 @@ import { isEmpty } from 'lodash'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Button from '@/Components/Button'
import FormInput from '@/Components/FormInput'
export default function Form(props) {
const { customer, levels } = props
const { data, setData, post, processing, errors, reset, clearErrors } =
useForm({
level: '',
image_prove_url: '',
level: customer.level.key,
paylater_limit: +customer.paylater_limit,
})
const handleOnChange = (event) => {
@ -56,7 +57,22 @@ export default function Form(props) {
customer
)}
>
{customer.name}
{customer.fullname} (
{customer.username})
</Link>
</td>
</tr>
<tr>
<td className="font-bold">Whatsapp</td>
<td>:</td>
<td className="hover:underline">
<Link
href={route(
'customer.edit',
customer
)}
>
{customer.phone}
</Link>
</td>
</tr>
@ -70,7 +86,7 @@ export default function Form(props) {
/>
</div>
<div className="my-4">
<div className="mt-4">
<div className="mb-1 text-sm">Level Upgrade</div>
<select
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
@ -85,6 +101,21 @@ export default function Form(props) {
</option>
))}
</select>
{errors.level && (
<p className="mb-2 text-sm text-red-600 dark:text-red-500">
{errors.level}
</p>
)}
</div>
<div className="mb-4 mt-2">
<FormInput
type="number"
label="Limit Hutang"
name="paylater_limit"
onChange={handleOnChange}
value={data.paylater_limit}
error={errors.paylater_limit}
/>
</div>
<div className="flex items-center">
<Button

@ -96,6 +96,7 @@ Route::middleware(['http_secure_aware', 'inertia.admin'])
Route::get('/customers/{customer}', [CustomerController::class, 'edit'])->name('customer.edit');
Route::post('/customers/{customer}', [CustomerController::class, 'update'])->name('customer.update');
Route::delete('/customers/{customer}', [CustomerController::class, 'destroy'])->name('customer.destroy');
Route::post('/customers/{customer}/level', [CustomerController::class, 'update_level'])->name('customer.update_level');
// voucher
Route::get('/vouchers/import', [VoucherController::class, 'form_import'])->name('voucher.form_import');

Loading…
Cancel
Save