point customer done

dev
Aji Kamaludin 10 months ago
parent 2f7bbee0ce
commit 0abb8f5a07
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

@ -18,7 +18,7 @@ class CustomerController extends Controller
->orWhere('code', 'like', "%{$request->q}%");
}
$query->orderBy('created_at', 'desc');
$query->orderBy('updated_at', 'desc');
return inertia('Customer/Index', [
'query' => $query->paginate(),
@ -47,7 +47,7 @@ class CustomerController extends Controller
public function update(Request $request, Customer $customer)
{
$request->validate([
'code' => 'required|string|max:255|unique:customers,code',
'code' => 'required|string|max:255|unique:customers,code,' . $customer->id,
'name' => 'required|string|max:255',
'point' => 'required|numeric',
]);

@ -2,23 +2,85 @@
namespace App\Http\Controllers;
use App\Imports\CustomerPointsImport;
use App\Models\Customer;
use App\Models\CustomerPoint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
class CustomerPointController extends Controller
{
public function index()
{
}
public function store()
public function index(Request $request)
{
$query = CustomerPoint::with(['customer'])->orderBy('created_at', 'desc');
if ($request->customer_id != '') {
$query->where('customer_id', $request->customer_id);
}
if ($request->q != '') {
$query->whereHas('customer', function ($query) use ($request) {
$query->where('name', 'like', "%{$request->q}%")
->orWhere('code', 'like', "%{$request->q}%");
});
}
return inertia('CustomerPoint/Index', [
'query' => $query->paginate()
]);
}
public function update()
public function create(Request $request)
{
$customer = Customer::query()->orderBy('updated_at', 'desc');
if ($request->q != '') {
$customer->where('name', 'like', "%{$request->q}%")
->orWhere('code', 'like', "%{$request->q}%");
}
return inertia('CustomerPoint/Form', [
'customers' => $customer->paginate(10)
]);
}
public function destroy()
public function store(Request $request)
{
$request->validate([
'items' => 'required|array',
'items.*.customer_id' => 'required|exists:customers,id',
'items.*.point' => 'required|numeric',
'items.*.description' => 'nullable|string'
]);
DB::beginTransaction();
foreach ($request->items as $item) {
$customer = Customer::find($item['customer_id']);
$customer->update([
'last_point' => $customer->last_point + $item['point']
]);
$customer->points()->create([
'point' => $item['point'],
'description' => $item['description']
]);
}
DB::commit();
return redirect()->route('customer-point.index')
->with('message', ['type' => 'success', 'message' => 'Create item success']);
}
public function import()
public function import(Request $request)
{
$request->validate([
'file' => 'required|file'
]);
Excel::import(new CustomerPointsImport, $request->file('file'));
return redirect()->route('customer-point.index')
->with('message', ['type' => 'success', 'message' => 'Import Success']);
}
}

@ -0,0 +1,33 @@
<?php
namespace App\Imports;
use App\Models\Customer;
use App\Models\CustomerPoint;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
class CustomerPointsImport implements ToModel, WithHeadingRow
{
/**
* @param array $row
*
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
$customer = Customer::where('code', $row['code'])->first();
if ($customer != null) {
$customer->update([
'last_point' => $customer->last_point + $row['point']
]);
return new CustomerPoint([
'customer_id' => $customer->id,
'description' => $row['description'],
'point' => $row['point'],
]);
}
}
}

@ -11,4 +11,9 @@ class Customer extends Model
'start_point',
'last_point',
];
public function points()
{
return $this->hasMany(CustomerPoint::class);
}
}

@ -9,4 +9,9 @@ class CustomerPoint extends Model
'description',
'point',
];
public function customer()
{
return $this->belongsTo(Customer::class);
}
}

@ -15,8 +15,8 @@ return new class extends Migration
$table->ulid('id')->primary();
$table->string('name');
$table->string('code')->unique();
$table->decimal('start_point', 20, 2);
$table->decimal('last_point', 20, 2);
$table->decimal('start_point', 20, 2)->default(0);
$table->decimal('last_point', 20, 2)->default(0);
$table->timestamps();
$table->softDeletes();
$table->ulid('created_by')->nullable();

@ -14,8 +14,8 @@ return new class extends Migration
Schema::create('customer_points', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->ulid('customer_id');
$table->string('description', 1000);
$table->decimal('point', 20, 2);
$table->string('description', 1000)->nullable();
$table->decimal('point', 20, 2)->default(0);
$table->timestamps();
$table->softDeletes();
$table->ulid('created_by')->nullable();

@ -39,9 +39,7 @@ class PermissionSeeder extends Seeder
['id' => Str::ulid(), 'label' => 'Delete Customer', 'name' => 'delete-customer'],
['id' => Str::ulid(), 'label' => 'Create Customer Point', 'name' => 'create-customer-point'],
['id' => Str::ulid(), 'label' => 'Update Customer Point', 'name' => 'update-customer-point'],
['id' => Str::ulid(), 'label' => 'View Customer Point', 'name' => 'view-customer-point'],
['id' => Str::ulid(), 'label' => 'Delete Customer Point', 'name' => 'delete-customer-point'],
];
foreach ($permissions as $permission) {

@ -0,0 +1,5 @@
code,point,description
AACRB400,1,'some note'
AAISU221,20,
AAISU221,-10,
AAMMZ012,-2,
1 code point description
2 AACRB400 1 'some note'
3 AAISU221 20
4 AAISU221 -10
5 AAMMZ012 -2

@ -30,7 +30,7 @@ export default [
show: true,
icon: HiTrophy,
route: route('customer-point.index'),
active: 'customer-point.index',
active: 'customer-point.*',
permission: 'view-customer-point',
},
{

@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react'
import { router, usePage } from '@inertiajs/react'
import { Spinner } from 'flowbite-react'
import { usePrevious } from 'react-use'
import Modal from '@/Components/Modal'
import SearchInput from '@/Components/SearchInput'
import Pagination from '@/Components/Pagination'
export default function CustomerSelectionModal(props) {
const { modalState, onItemSelected } = props
const [loading, setLoading] = useState(false)
const {
props: {
customers: { data, links },
},
} = usePage()
const [search, setSearch] = useState('')
const preValue = usePrevious(search)
const params = { customer_q: search }
const handleItemSelected = (item) => {
onItemSelected(item)
modalState.toggle()
}
useEffect(() => {
if (preValue) {
router.get(route(route().current()), params, {
replace: true,
preserveState: true,
})
}
}, [search])
useEffect(() => {
router.on('start', () => setLoading(true))
router.on('finish', () => setLoading(false))
}, [])
return (
<Modal
isOpen={modalState.isOpen}
toggle={modalState.toggle}
title={'Customer'}
>
<SearchInput
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
{loading ? (
<div className="justify-center flex flex-row w-full py-32">
<Spinner size="xl" />
</div>
) : (
<div className="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">
<tr>
<th scope="col" className="py-3 px-6">
Name
</th>
</tr>
</thead>
<tbody>
{data.map((customer) => (
<tr
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
key={customer.id}
onClick={() =>
handleItemSelected(customer)
}
>
<td
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap hover:bg-gray-200"
>
{customer.name}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="w-full flex items-center justify-center">
<Pagination links={links} params={params} />
</div>
</div>
)}
</Modal>
)
}

@ -0,0 +1,156 @@
import React from 'react'
import { Head, useForm } from '@inertiajs/react'
import { useModalState } from '@/hooks'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Button from '@/Components/Button'
import CustomerSelectionModal from './CustomerSelectionModal'
import FormInput from '@/Components/FormInput'
import { HiXCircle } from 'react-icons/hi2'
export default function Setting(props) {
const { data, setData, post, processing, errors } = useForm({
items: [],
})
const customerSelectionModal = useModalState()
const addItem = (customer) => {
const isExists = data.items.find((i) => i.customer.id === customer.id)
if (!isExists) {
let items = data.items.concat({
customer: customer,
customer_id: customer.id,
point: 0,
description: '',
})
setData('items', items)
}
}
const removeItem = (index) => {
let items = data.items.filter((_, i) => i !== index)
setData('items', items)
}
const handleChangeValue = (name, value, index) => {
setData(
'items',
data.items.map((item, i) => {
if (i === index) {
item[name] = value
}
return item
})
)
}
const handleSubmit = () => {
post(route('customer-point.store'))
}
return (
<AuthenticatedLayout
auth={props.auth}
errors={props.errors}
flash={props.flash}
page={'Point'}
action={'Form'}
>
<Head title="Point" />
<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">Point</div>
<div className="border rounded-md p-2">
<Button
size="sm"
onClick={customerSelectionModal.toggle}
>
Tambah
</Button>
<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">
<tr>
<th scope="col" className="py-3 px-6">
Customer
</th>
<th scope="col" className="py-3 px-6">
Point
</th>
<th scope="col" className="py-3 px-6">
Description
</th>
<th />
</tr>
</thead>
<tbody>
{data.items.map((item, index) => (
<tr
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
key={index}
>
<td
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{item.customer.name} ({' '}
{item.customer.code} )
</td>
<td className="py-4 px-6">
<FormInput
type="number"
value={item.point}
onChange={(e) =>
handleChangeValue(
'point',
e.target.value,
index
)
}
/>
</td>
<td className="py-4 px-6">
<FormInput
value={item.description}
onChange={(e) =>
handleChangeValue(
'description',
e.target.value,
index
)
}
/>
</td>
<td className="py-4 px-6">
<HiXCircle
className="w-5 h-5 text-red-600"
onClick={() =>
removeItem(index)
}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2">
<Button
onClick={handleSubmit}
processing={processing}
>
Simpan
</Button>
</div>
</div>
</div>
</div>
<CustomerSelectionModal
onItemSelected={addItem}
modalState={customerSelectionModal}
/>
</AuthenticatedLayout>
)
}

@ -0,0 +1,102 @@
import React, { useEffect, useRef } from 'react'
import { useForm } from '@inertiajs/react'
import Modal from '@/Components/Modal'
import Button from '@/Components/Button'
export default function ImportModal(props) {
const { modalState } = props
const { data, setData, post, progress, processing, errors, clearErrors } =
useForm({
file: null,
})
const inputFileImport = useRef()
const handleReset = () => {
setData({ file: null })
inputFileImport.current.value = ''
clearErrors()
}
const handleCancel = () => {
modalState.toggle()
handleReset()
}
const handleClose = () => {
handleReset()
modalState.toggle()
}
function handleSubmit(e) {
e.preventDefault()
post(route('customer-point.import'), {
forceFormData: false,
onSuccess: () => Promise.all([handleReset(), modalState.toggle()]),
})
return
}
return (
<Modal
isOpen={modalState.isOpen}
toggle={handleClose}
title={'Import Point'}
>
<div
className={`flex flex-row items-center gap-2 border rounded-md ${
errors.file && 'border-red-600'
}`}
onClick={() => {
console.log(inputFileImport.current.click())
}}
>
<div className="px-2 py-1 bg-gray-200 hover:bg-gray-400 font-bold rounded-l-md">
Pilih File:{' '}
</div>
<div>{data.file ? data.file.name : 'Pilih File'}</div>
</div>
<div className="text-sm text-red-600">{errors.file}</div>
<input
ref={inputFileImport}
type="file"
className="hidden"
onChange={(e) => setData('file', e.target.files[0])}
/>
{progress && (
<div className="w-full bg-gray-200 rounded-full dark:bg-gray-700">
<div
className="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
style={{ width: progress.percentage + '%' }}
>
{' '}
{progress.percentage}%
</div>
</div>
)}
<p className="text-sm text-gray-500">
Unduh format file import{' '}
<a
className="underline text-blue-500"
href="/point.csv"
download="point.csv"
>
disini
</a>
</p>
<div className="flex justify-between mt-4 space-x-4">
<Button onClick={handleSubmit} processing={processing}>
Upload
</Button>
<Button
type="secondary"
onClick={handleCancel}
processing={processing}
>
Batal
</Button>
</div>
</Modal>
)
}

@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react'
import { router, Link } from '@inertiajs/react'
import { usePrevious } from 'react-use'
import { Head } from '@inertiajs/react'
import { Button } from 'flowbite-react'
import { useModalState } from '@/hooks'
import { hasPermission } from '@/utils'
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import Pagination from '@/Components/Pagination'
import SearchInput from '@/Components/SearchInput'
import ImportModal from './ImportModal'
import CustomerSelectionInput from '../Customer/SelectionInput'
export default function Customer(props) {
const {
query: { links, data },
auth,
} = props
const [search, setSearch] = useState('')
const [customer_id, setCustomerId] = useState(null)
const preValue = usePrevious(`${search}-${customer_id}`)
const importModal = useModalState()
const toggleImportModal = () => {
importModal.toggle()
}
const params = { q: search, customer_id: customer_id }
useEffect(() => {
if (preValue) {
router.get(
route(route().current()),
{ q: search, customer_id: customer_id },
{
replace: true,
preserveState: true,
}
)
}
}, [search, customer_id])
const canCreate = hasPermission(auth, 'create-customer-point')
return (
<AuthenticatedLayout
auth={props.auth}
errors={props.errors}
flash={props.flash}
page={'Point'}
action={'List'}
>
<Head title="Point" />
<div>
<div className="mx-auto sm:px-6 lg:px-8 ">
<div className="p-6 overflow-hidden shadow-sm sm:rounded-lg bg-gray-200 dark:bg-gray-800 space-y-4">
<div className="flex flex-col md:flex-row justify-between gap-2">
{canCreate && (
<div className="flex flex-row gap-2 mt-1.5">
<Link
href={route('customer-point.create')}
className="text-white bg-blue-700 border border-transparent hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 disabled:hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 dark:disabled:hover:bg-blue-600 group flex h-min items-center justify-center p-0.5 text-center font-medium focus:z-10 rounded-lg"
>
<span className="flex items-center rounded-md text-sm px-3 py-1.5">
Create
</span>
</Link>
<Button
size="sm"
outline
onClick={() => toggleImportModal()}
>
Import
</Button>
</div>
)}
<div className="flex flex-col md:flex-row gap-1 items-center">
<div className="mt-1.5 w-full">
<CustomerSelectionInput
itemSelected={customer_id}
onItemSelected={(id) =>
setCustomerId(id)
}
/>
</div>
<div className="w-full">
<SearchInput
onChange={(e) =>
setSearch(e.target.value)
}
value={search}
/>
</div>
</div>
</div>
<div className="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">
<tr>
<th
scope="col"
className="py-3 px-6"
>
Customer
</th>
<th
scope="col"
className="py-3 px-6"
>
Point
</th>
<th
scope="col"
className="py-3 px-6"
>
Description
</th>
</tr>
</thead>
<tbody>
{data.map((point) => (
<tr
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
key={point.id}
>
<td
scope="row"
className="py-4 px-6 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{point.customer.name} ({' '}
{point.customer.code} )
</td>
<td className="py-4 px-6">
{point.point}
</td>
<td className="py-4 px-6">
{point.description}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="w-full flex items-center justify-center">
<Pagination links={links} params={params} />
</div>
</div>
</div>
</div>
</div>
<ImportModal modalState={importModal} />
</AuthenticatedLayout>
)
}

@ -52,9 +52,8 @@ Route::middleware(['auth'])->group(function () {
// Customer Point
Route::get('/customers-points', [CustomerPointController::class, 'index'])->name('customer-point.index');
Route::post('/customers-points/import', [CustomerPointController::class, 'import'])->name('customer-point.import');
Route::get('/customers-points/create', [CustomerPointController::class, 'create'])->name('customer-point.create');
Route::post('/customers-points', [CustomerPointController::class, 'store'])->name('customer-point.store');
Route::put('/customers-points/{customer}', [CustomerPointController::class, 'update'])->name('customer-point.update');
Route::delete('/customers-points/{customer}', [CustomerPointController::class, 'destroy'])->name('customer-point.destroy');
});
Route::middleware('auth')->group(function () {

Loading…
Cancel
Save