Compare commits

...

2 commits

Author SHA1 Message Date
Anna-Sara
cfb199ce48
Merge pull request #1 from anna-sara/Create_groups
Feature: Create groups
2026-01-07 17:20:59 +01:00
Anna-Sara Sélea
8770925309 Feature: Create groups 2026-01-07 17:20:04 +01:00
20 changed files with 1019 additions and 82 deletions

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\CustomerGroup;
use Illuminate\Http\Request;
use App\Models\Purchase;
use Inertia\Inertia;
@ -14,7 +15,8 @@ class CustomerController extends Controller
*/
public function index()
{
$customers = Customer::orderBy('name', 'asc')->get();
$customers = Customer::where('is_in_group', 0)->orderBy('name', 'asc')->get();
return Inertia::render('Dashboard', ['customers' => $customers]);
}
@ -52,16 +54,22 @@ class CustomerController extends Controller
public function show($id)
{
$customer = Customer::with('purchases')->with('deposits')->findOrFail($id);
$groupmembers = Customer::where('is_in_group', 1)->where('customer_group_id', $customer->customer_group_id)->get();
return Inertia::render('Customer', ['customer' => $customer]);
return Inertia::render('Customer', ['customer' => $customer, 'groupmembers' => $groupmembers]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Customers $customers)
public function edit($id)
{
//
$customer = Customer::findOrFail($id);
$customer->customer_group_id = null;
$customer->is_in_group = 0;
$customer->save();
}
/**
@ -112,4 +120,5 @@ class CustomerController extends Controller
'success' => true, 'message' => 'Customer deleted successfully'
]);
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers;
use App\Models\CustomerGroup;
use App\Models\Customer;
use App\Models\Deposit;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CustomerGroupController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$groups = CustomerGroup::with('customers')->get();
$customers = Customer::whereNull('customer_group_id')->where('is_in_group', false)->orderBy('name', 'asc')->get();
return Inertia::render('CustomerGroups', ['groups' => $groups, 'customers' => $customers]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'group_name' => 'required',
'customers' => 'required',
]);
$customers = $request->customers;
$guardian_name = Customer::where('id', (int) $customers[0])->pluck('guardian_name');
$customerGroup = CustomerGroup::create([
'name' => $request->group_name,
]);
$groupCustomer = Customer::create([
'name' => $request->group_name,
'guardian_name' => $guardian_name[0],
'is_in_group' => false,
'customer_group_id' => $customerGroup->id
]);
$groupAmount= 0;
foreach ($customers as $customerItem) {
$customer = Customer::findOrFail($customerItem);
$customer->customer_group_id = $customerGroup->id;
$customer->is_in_group = 1;
$groupAmount += $customer->deposit;
$customer->deposit = 0;
$customer->save();
}
Deposit::create([
'customer_id' => $groupCustomer->id,
'amount' => $groupAmount,
]);
$groupCustomer->amount_left = $groupAmount;
$groupCustomer->deposit = $groupAmount;
$groupCustomer->save();
return redirect('customer-groups/');
}
/**
* Display the specified resource.
*/
public function show(CustomerGroup $customerGroup)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(CustomerGroup $customerGroup)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update($id, Request $request)
{
$request->validate([
'customers' => 'required',
]);
$customers = $request->customers;
$groupCustomer = Customer::where('customer_group_id', $id)->where('is_in_group', 0)->first();
foreach ($customers as $customerItem) {
$customer = Customer::findOrFail($customerItem);
$groupCustomer->deposit += $customer->deposit;
$groupCustomer->amount_left += $customer->deposit;
$groupCustomer->save();
$customer->customer_group_id = $id;
$customer->is_in_group = 1;
$customer->deposit = 0;
$customer->save();
}
return response()->json([
'success' => true, 'message' => 'Customer group was updated'
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
$group = CustomerGroup::findOrFail( $id );
$customerGroup = Customer::where('customer_group_id', $group->id);
$customerGroup->delete();
$group->delete();
return response()->json([
'success' => true, 'message' => 'Customer group deleted successfully'
]);
}
}

View file

@ -41,11 +41,36 @@ class DepositController extends Controller
]);
$customer = Customer::findOrFail($request->customer_id);
$customer->deposit = $customer->deposit + $request->deposit;
$customer->amount_left = $customer->amount_left + $request->deposit;
$customer->save();
return redirect('customer/' . $request->customer_id);
if ($customer->is_in_group) {
$groupCustomer = Customer::where('customer_group_id', $customer->customer_group_id)->where('is_in_group', 0)->first();
$groupCustomer->deposit += $customer->deposit;
$groupCustomer->amount_left += $request->deposit;
$groupCustomer->save();
$customer->deposit = 0;
$customer->save();
if ($request->manual_deposit === 1) {
return redirect('customer/' . $request->customer_id);
}
return response()->json([
'success' => true, 'message' => 'Deposit added successfully'
]);
} else {
$customer->deposit = $customer->deposit + $request->deposit;
$customer->amount_left = $customer->amount_left + $request->deposit;
$customer->save();
if ($request->manual_deposit === 1) {
return redirect('customer/' . $request->customer_id);
}
return response()->json([
'success' => true, 'message' => 'Deposit added successfully'
]);
}
}
/**

View file

@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->header('X-API-KEY') === config('app.apikey_deposit')) {
return $next($request);
} else {
return response()->json(['code' => 401, 'message' => 'Unauthorized']);
}
}
}

View file

@ -14,13 +14,16 @@ class Customer extends Model
* @var array<int, string>
*/
protected $fillable = [
'lan_id',
'name',
'guardian_name',
'amount left',
'amount used',
'deposit',
'give_leftover',
'comment'
'comment',
'customer_group_id',
'is_in_group'
];
/**

View file

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\Customer;
class CustomerGroup extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
];
/**
* Get the customers for the group.
*/
public function customers()
{
return $this->hasMany(Customer::class);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tableversion extends Model
{
protected $fillable = [
'table',
'version'
];
}

View file

@ -123,4 +123,9 @@ return [
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
'apikey_deposit' => env('API_KEY_DEPOSIT'),
'apilan_key' => env('API_LAN_KEY'),
'apilan_url' => env('API_LAN_URL'),
'apilan_clientcert_path' => env('API_LAN_CLIENTCERT_PATH')
];

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->boolean('is_in_group')->default(false);
$table->integer('customer_group_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn('is_in_group');
$table->dropColumn('customer_group_id');
});
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('customer_groups', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('customer_groups');
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->integer('lan_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn('lan_id');
});
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tableversions', function (Blueprint $table) {
$table->id();
$table->string('table')->nullable();
$table->integer('version')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tableversions');
}
};

View file

@ -91,6 +91,10 @@ details {
right: 1em;
background: #ffffff;
&.group {
top: 0.5em;
}
svg {
display: block;
}
@ -113,3 +117,41 @@ details {
display: none;
}
}
.customer-box:nth-child(even) {
background-color:rgba(0, 128, 187, 0.075)
}
.customers-list {
max-height: 200px;
overflow-y: scroll;
padding: 5px;}
.add-customer-to-group-modal {
//display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
}
.add-customer-to-group-modal-content {
background-color: #fefefe;
margin: auto;
padding: 40px;
width: 60%;
position: relative;
.delete {
position: absolute;
top: 20px;
right: 20px;
}
}

View file

@ -24,15 +24,23 @@ export default function Authenticated({
<img className="navbar-logo" src="/img/logo.png" />
</Link>
</div>
{/*
<div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<NavLink
href={route('dashboard')}
href="/dashboard"
active={route().current('dashboard')}
>
Dashboard
Deltagare
</NavLink>
</div>*/}
</div>
<div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<NavLink
href="/customer-groups"
active={route().current('customer-groups')}
>
Grupper
</NavLink>
</div>
</div>
<div className="hidden sm:ms-6 sm:flex sm:items-center">

View file

@ -1,6 +1,5 @@
import TextInput from '@/Components/TextInput';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Textarea } from '@headlessui/react';
import { Head, useForm } from '@inertiajs/react';
import axios from 'axios';
import { FormEventHandler } from 'react';
@ -8,6 +7,7 @@ import { FormEventHandler } from 'react';
interface CustomerProps {
customer:{
id: number
lan_id: number,
name: string
deposit: number
amount_left: number
@ -25,16 +25,31 @@ interface CustomerProps {
}
};
export default function Customer({customer}: CustomerProps) {
interface GroupmembersProps {
groupmembers:[{
id: number
lan_id: number,
name: string
deposit: number
amount_left: number
give_leftover: number
guardian_name: string
comment: string;
}]
};
export default function Customer({customer, groupmembers}: (CustomerProps & GroupmembersProps) ) {
const { data, setData, post, processing, errors, reset } = useForm({
amount: "",
customer_id: customer.id,
deposit: "",
id: customer.id,
comment: ""
comment: "",
manual_deposit: 0
});
const submit: FormEventHandler = (e) => {
e.preventDefault()
post(route('register_purchase'), {
@ -47,10 +62,11 @@ export default function Customer({customer}: CustomerProps) {
const submitDeposit: FormEventHandler = (e) => {
e.preventDefault()
post(route('register_deposit'), {
headers: {'X-API-KEY': '123', 'Accept': 'application/json', 'Content-Type': 'application/json'},
onFinish: () => setData(
'deposit', ''
),
});
})
}
const updateComment: FormEventHandler = (e) => {
@ -73,13 +89,24 @@ export default function Customer({customer}: CustomerProps) {
<section className='section'>
<div className="container is-max-desktop">
<h1 className="title is-2">{customer.name}</h1>
<a href="/dashboard" className="button mb-5 is-small is-white">Tillbaka</a>
<h1 className="title is-2">{customer.lan_id ? customer.lan_id + "." : "Grupp"} {customer.name}</h1>
<div className='container is-centered'>
<div className="box">
<h2 className='title is-4'>Saldo: {customer.amount_left ? customer.amount_left : 0} kr</h2>
<h2 className='title is-4'>Saldo: {customer.amount_left ? customer.amount_left : 0} kr</h2>
<p>Inbetalad summa: {customer.deposit ? customer.deposit : 0} kr</p>
<p>Vårnadshavare: {customer.guardian_name}</p>
<p>Ge ev överblivet saldo till vBytes: {customer.give_leftover ? "Ja" : "Nej"}</p>
{groupmembers.length > 0 &&
<>
<h2 className='title is-5 mt-5 mb-2'>Gruppmedlemmar</h2>
{groupmembers && groupmembers.map( member => {
console.log(member)
return <p>{member.lan_id}. {member.name} </p>
})}
</>
}
</div>
<div className="box">
@ -144,44 +171,44 @@ export default function Customer({customer}: CustomerProps) {
</svg>
</div>
</details>
<details className="box">
<summary className='title is-4 my-3'>
<span>Inbetalning Swish/kontant</span>
<div className="summary-chevron-up">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</summary>
<form onSubmit={submitDeposit}>
<div className="field">
<div className="control">
<TextInput
required
className="input"
type="number"
name="deposit"
value={data.deposit}
placeholder="Summa"
onChange={(e) => setData('deposit', e.target.value)}
/>
{groupmembers.length < 1 &&
<details className="box">
<summary className='title is-4 my-3'>
<span>Inbetalning Swish/kontant</span>
<div className="summary-chevron-up">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button">Spara</button>
</summary>
<form onSubmit={submitDeposit}>
<div className="field">
<div className="control">
<TextInput
required
className="input"
type="number"
name="deposit"
value={data.deposit}
placeholder="Summa"
onChange={(e) => [setData('deposit', e.target.value), setData('manual_deposit', 1)]}
/>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button">Spara</button>
</div>
</div>
</form>
<div className="summary-chevron-down">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-up">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</div>
</form>
<div className="summary-chevron-down">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-up">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</div>
</details>
</details>
}
<details className="box">

View file

@ -0,0 +1,384 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import TextInput from '@/Components/TextInput';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler, SetStateAction, useState } from 'react';
import axios from 'axios';
import Customer from './Customer';
interface CustomerProps {
customers: [{
id: number
name: string
customer_group_id: string,
is_in_group: boolean
}],
};
type Customer = {
id: number
name: string
customer_group_id: string,
is_in_group: boolean
};
interface CustomerGroupProps {
groups: [{
name: string,
id: number,
customers: [{
id: number
name: string
customer_group_id: string,
is_in_group: boolean
}],
}]
};
type CustomerGroup = {
name: string,
id: number,
customers: [{
id: number
name: string
customer_group_id: string,
is_in_group: boolean
}],
};
type FormInputs = {
id: number | null;
customers: Customer[],
group_name: string,
};
interface CurrentGroup {
id?: number | null ;
name?: string;
}
export default function CustomerGroups({groups, customers} :( CustomerGroupProps & CustomerProps) ) {
const [addCustomerToGroupModal, setAddCustomerToGroupModal] = useState(false);
const [currentGroup, setCurrentGroup] = useState<CurrentGroup>({id: null, name: ""});
const [searchItemGroup, setSearchItemGroup] = useState('');
const [searchItemCustomers, setSearchItemCustomers] = useState('')
const [searchItemCustomersAddToGroup, setSearchItemCustomersAddToGroup] = useState('')
const [uniqueGroupNameError, setUniqueGroupNameError,] = useState(false)
const [activeTabLetter, setActiveTabLetter] = useState('')
const [activeTab, setActiveTab] = useState('groups')
const [filteredCustomers, setFilteredCustomers] = useState<Customer[]>(customers)
const [filteredCustomersAddToGroup, setFilteredCustomersAddToGroup] = useState<Customer[]>(customers)
const [filteredGroups, setFilteredGroups] = useState<CustomerGroup[]>(groups)
const [checkboxes, setCheckboxes] = useState<any[]>([]);
const [checkboxesAddToGroup, setCheckboxesAddToGroup] = useState<any[]>([]);
const letterArray = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","Å","Ä","Ö"];
const { data, setData, post, processing, errors, reset } = useForm<FormInputs>({
id: null,
group_name: "",
customers: []
});
//Group
const handleInputChangeCreateGroup = (e: any) => {
const searchTerm = e.target.value;
setSearchItemCustomers(searchTerm)
const filteredItems = customers.filter((customer) =>
customer.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredCustomers(filteredItems);
}
const submitGroup: FormEventHandler = (e : any) => {
e.preventDefault()
let submit = true;
groups.map( group => {
if (data.group_name.toLowerCase() === group.name.toLowerCase()) {
setUniqueGroupNameError(true);
submit = false;
}
})
if (submit) {
post(route('register_customer_group'), {
onFinish: () => [setData(
'group_name', '',
), setCheckboxes([]),
window.location.href = "/customer-groups"],
});
}
}
const handleInputChangeGroup = (e: any) => {
const searchTerm = e.target.value;
setSearchItemGroup(searchTerm)
const filteredItems = groups.filter((customer) =>
customer.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredGroups(filteredItems);
}
const handleInputClick = (searchTerm: string) => {
const filteredItems = groups.filter((customer) =>
customer.name.toLowerCase().startsWith(searchTerm.toLowerCase())
);
setFilteredGroups(filteredItems);
}
const handleCheckBoxChange = (e: any) => {
let index = e.target.dataset.id;
let prev = checkboxes;
let itemIndex = prev.indexOf(index);
if (itemIndex !== -1) {
prev.splice(itemIndex, 1);
} else {
prev.push(index);
}
setCheckboxes([...prev]);
setData('customers', checkboxes)
}
const deleteCustomerGroup = (id: string | number | undefined) => {
axios.delete('/api/customer-group/' + id)
.then(response => {
window.location.href = "/customer-groups";
})
.catch(error => {console.log(error)})
}
// Add to group
const handleInputChangeAddToGroup = (e: any) => {
const searchTerm = e.target.value;
setSearchItemCustomersAddToGroup(searchTerm)
const filteredItems = customers.filter((customer) =>
customer.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredCustomersAddToGroup(filteredItems);
}
const addToGroup = (id: number | null | undefined) => {
axios.post('/api/add-to-customer-group/' + id, {customers: data.customers})
.then(response => {
window.location.href = "/customer-groups";
})
.catch(error => {console.log(error)})
}
const handleCheckBoxChangeAddToGroup = (e: any) => {
let index = e.target.dataset.id;
let prev = checkboxesAddToGroup;
let itemIndex = prev.indexOf(index);
if (itemIndex !== -1) {
prev.splice(itemIndex, 1);
} else {
prev.push(index);
}
setCheckboxesAddToGroup([...prev]);
setData('customers', checkboxesAddToGroup)
}
const deleteCustomerFromGroup = (id: string | number) => {
axios.put('/api/customer/' + id)
.then(response => {
window.location.href = "/customer-groups";
})
.catch(error => {console.log(error)})
}
return (
<AuthenticatedLayout>
<Head title="Dashboard" />
{ addCustomerToGroupModal &&
<div className='add-customer-to-group-modal'>
<div className='add-customer-to-group-modal-content'>
<button onClick={() => setAddCustomerToGroupModal(false)} className="delete is-large" aria-label="close"></button>
<form className='container is-centered'>
<h2 className="mt-4 title is-3" >Lägg till deltagare i grupp {currentGroup.name}</h2>
<div className='customers-list is-flex is-flex-direction-column gap-3 container is-centered my-4'>
{filteredCustomersAddToGroup && filteredCustomersAddToGroup
.map( customer => {
return <label className='checkbox'>
<input type="checkbox" className='mr-2' data-id={customer.id} onClick={handleCheckBoxChangeAddToGroup}/>
{customer.name}
</label>
})}
</div>
<input
className='input'
type="text"
value={searchItemCustomersAddToGroup}
onChange={handleInputChangeAddToGroup}
placeholder='Skriv för att söka deltagare/funktionär'
/>
<div className='is-flex mt-3'>
<span className='mr-2 has-text-weight-bold'>Valda deltagare/funktionärer: </span>
{filteredCustomersAddToGroup && filteredCustomersAddToGroup
.filter((filteredCustomer) => checkboxesAddToGroup.includes("" + filteredCustomer.id))
.map( customer => {
return <p key={customer.id} className='mr-2 tag'>
{customer.name}
</p>
})}
</div>
<button onClick={() => addToGroup(currentGroup.id)} className="button mt-4 is-small is-white">Lägg till deltagare</button>
</form>
</div>
</div>
}
<section className='section'>
<div className="container is-max-desktop">
<a href="/dashboard" className="button mb-5 is-small is-white">Tillbaka</a>
<div className="tabs is-centered is-boxed">
<ul>
<li className={activeTab === "groups" ? "is-active" : ""}>
<a onClick={() => setActiveTab('groups') }>
<span>Grupper</span>
</a>
</li>
<li className={activeTab === "create-group" ? "is-active" : ""}>
<a onClick={() => setActiveTab('create-group') }>
<span>Skapa grupp</span>
</a>
</li>
</ul>
</div>
{activeTab === "create-group" && <>
<h2 className="title is-2">Skapa grupp</h2>
<div className='create-group-container mb-7'>
<div>
<form onSubmit={submitGroup} className='container is-centered'>
<p className="mb-4">Namn grupp:</p>
<TextInput
required
className="input"
type="text"
name="group_name"
value={data.group_name}
placeholder="Namn på grupp"
min={0}
//max={customer.group_name_left}
onChange={(e) => setData('group_name', e.target.value)}
/>
{uniqueGroupNameError && <p>Gruppnamn är inte unikt</p>}
<p className="mt-4" >Välj vilka som ska ingå i gruppen:</p>
<div className='customers-list is-flex is-flex-direction-column gap-3 container is-centered my-4'>
{filteredCustomers && filteredCustomers
.map( customer => {
return <label key={customer.id} className='checkbox'>
<input type="checkbox" className='mr-2' data-id={customer.id} onClick={handleCheckBoxChange}/>
{customer.name}
</label>
})}
</div>
<input
className='input'
type="text"
value={searchItemCustomers}
onChange={handleInputChangeCreateGroup}
placeholder='Skriv för att söka deltagare/funktionär'
/>
<div className='is-flex mt-3'>
<span className='mr-2 has-text-weight-bold'>Valda deltagare/funktionärer: </span>
{filteredCustomers && filteredCustomers
.filter((filteredCustomer) => checkboxes.includes("" + filteredCustomer.id))
.map( customer => {
return <p key={customer.id} className='mr-2 tag'>
{customer.name}
</p>
})}
</div>
<button className="button mt-4 is-small is-white">Skapa grupp</button>
</form>
</div>
</div>
</>
}
{ activeTab === "groups" && <>
<h2 className="title is-2">Grupper</h2>
<input
className='input my-4'
type="text"
value={searchItemGroup}
onChange={handleInputChangeGroup}
placeholder='Skriv för att söka'
/>
<div>
<ul className='grid is-col-min-2 is-column-gap-1 mb-5'>
{ letterArray && letterArray.map( letter => {
return <li className={`${activeTabLetter == letter ? "is-active" : ""} cell button letter is-small is-white`} onClick={ () => [setActiveTabLetter(letter) ,handleInputClick(letter)]}>
{letter}
</li>
})
}
<li className='cell button is-small is-white' onClick={ () => [setActiveTabLetter(""), setFilteredGroups(groups), setSearchItemGroup("") ]}>
Rensa
</li>
</ul>
</div>
<div className='container is-centered'>
{filteredGroups && filteredGroups.map( group => {
return <details className="box">
<summary className='title is-4 my-3'>
<span>{group.name}</span>
<div className="summary-chevron-up">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</summary>
{ group.customers.length < 2 &&
<button onClick={() => deleteCustomerGroup(group.id)} className="button is-danger is-outlined is-small">
<span>Ta bort grupp</span>
</button>
}
<ul>
{ group.customers.length > 0 && group.customers.filter((customer) => customer.is_in_group).map( customer => {
return <div className='is-flex gap-4 border-b p-2 is-justify-content-space-between'>
<p className="is-size-5">{customer.name}</p>
<button onClick={() => deleteCustomerFromGroup(customer.id)} className="button is-danger is-outlined is-small">
<span>Ta bort från grupp</span>
</button>
</div>
})}
</ul>
<button className="button mt-4 is-small is-white" onClick={() => [setAddCustomerToGroupModal(true), setCurrentGroup({id: group.id, name: group.name})]}>Lägg till deltagare</button>
<div className="summary-chevron-down">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="feather feather-chevron-up">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</div>
</details>
})}
</div>
</>
}
</div>
</section>
</AuthenticatedLayout>
);
}

View file

@ -1,6 +1,6 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';
import { SetStateAction, useState } from 'react';
import { Head, useForm } from '@inertiajs/react';
import {useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
@ -9,21 +9,25 @@ interface CustomerProps {
customers: [{
id: number
name: string
group_id: string,
is_group: boolean
}],
};
interface Customer {
type Customer = {
id: number
name: string
group_id: string,
is_group: boolean
};
export default function Dashboard({customers}: CustomerProps) {
export default function Dashboard({ customers }: CustomerProps) {
const [searchItem, setSearchItem] = useState('')
const [activeTab, setActiveTab] = useState('')
const [filteredCustomers, setFilteredCustomers] = useState<Customer[]>(customers)
const letterArray = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","Å","Ä","Ö"];
const letterArray = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "Å", "Ä", "Ö"];
const handleInputChange = (e: any) => {
const searchTerm = e.target.value;
@ -44,6 +48,7 @@ export default function Dashboard({customers}: CustomerProps) {
setFilteredCustomers(filteredItems);
}
return (
<AuthenticatedLayout>
<Head title="Dashboard" />
@ -59,22 +64,22 @@ export default function Dashboard({customers}: CustomerProps) {
placeholder='Skriv för att söka'
/>
<div>
<ul className='grid is-col-min-2 is-column-gap-1 mb-5'>
{ letterArray && letterArray.map( letter => {
return <li className={`${activeTab == letter ? "is-active" : ""} cell button letter is-small is-white`} onClick={ () => [setActiveTab(letter) ,handleInputClick(letter)]}>
{letter}
</li>
<ul className='grid is-col-min-2 is-column-gap-1 mb-5'>
{letterArray && letterArray.map(letter => {
return <li className={`${activeTab == letter ? "is-active" : ""} cell button letter is-small is-white`} onClick={() => [setActiveTab(letter), handleInputClick(letter)]}>
{letter}
</li>
})
}
<li className='cell button is-small is-white' onClick={ () => [setActiveTab(""), setFilteredCustomers(customers), setSearchItem("") ]}>
Rensa
</li>
}
<li className='cell button is-small is-white' onClick={() => [setActiveTab(""), setFilteredCustomers(customers), setSearchItem("")]}>
Rensa
</li>
</ul>
</div>
<div className='container is-centered'>
{filteredCustomers && filteredCustomers.map( customer => {
return <a key={customer.id} className="box is-flex is-justify-content-space-between" href={`/customer/` + customer.id}>
{filteredCustomers && filteredCustomers.map(customer => {
return <a key={customer.id} className="box customer-box is-flex is-justify-content-space-between" href={`/customer/` + customer.id}>
<p>{customer.name}</p>
<span className="icon has-text-black">
<FontAwesomeIcon icon={faArrowRight} />

View file

@ -3,14 +3,22 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\CustomerGroupController;
use App\Http\Controllers\PurchaseController;
use App\Http\Controllers\DepositController;
use App\Http\Middleware\ApiToken;
Route::post('register_customer', [CustomerController::class, 'store'])->name('register_customer');
Route::middleware('auth:sanctum')->group(function () {
Route::post('register_deposit', [DepositController::class, 'store'])->name('register_deposit');
//Route::post('register_deposit', [DepositController::class, 'store'])->name('register_deposit');
Route::post('update_comment', [CustomerController::class, 'updateComment'])->name('update_comment');
Route::post('register_purchase', [PurchaseController::class, 'store'])->name('register_purchase');
Route::post('customer-group', [CustomerGroupController::class, 'store'])->name('register_customer_group');
Route::post('add-to-customer-group/{id}', [CustomerGroupController::class, 'update'])->name('add_to_customer_group');
Route::delete('customer-group/{id}', [CustomerGroupController::class, 'destroy'])->name('delete_customer_group');
Route::delete('customer/{id}', [CustomerController::class, 'destroy'])->name('delete_customer');
Route::put('customer/{id}', [CustomerController::class, 'edit'])->name('edit_customer');
});
Route::post('register_deposit', [DepositController::class, 'store'])->name('register_deposit')->middleware([ApiToken::class]);

View file

@ -2,7 +2,103 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Storage;
use App\Models\Customer;
use App\Models\Tableversion;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::call(function () {
$latestVersionParticipant = Tableversion::where('table', 'participants')->latest()->first();
$latestVersionVolunteer = Tableversion::where('table', 'volunteers')->latest()->first();
$client = new Client();
$responseVersions= $client->request(
'GET',
config('app.apilan_url') . "version",
[
'headers'=> [
'X-Api-Key' => config('app.apilan_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
//'cert' => config('app.apilan_clientcert_path')
'cert' => Storage::disk('public')->path('lan.vbytes.se.pem')
],
);
$versions = json_decode((string) $responseVersions->getBody(), true);
if ($latestVersionParticipant === null ) {
Tableversion::create([
'table' => 'participants',
'version' => $versions['participants'] - 1,
]);
$latestVersionParticipant = Tableversion::where('table', 'participants')->latest()->first();
}
if ( $latestVersionVolunteer === null ) {
Tableversion::create([
'table' => 'volunteers',
'version' => $versions['volunteers'] - 1,
]);
$latestVersionVolunteer = Tableversion::where('table', 'volunteers')->latest()->first();
}
if($latestVersionParticipant->version < $versions['participants'] || $latestVersionVolunteer->version < $versions['volunteers'] ) {
$response = $client->request(
'GET',
config('app.apilan_url') . "data",
[
'headers'=> [
'X-Api-Key' => config('app.apilan_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
//'cert' => config('app.apilan_clientcert_path')
'cert' => Storage::disk('public')->path('lan.vbytes.se.pem')
],
);
$response_data = json_decode((string) $response->getBody(), true);
foreach ($response_data['participants'] as $participant) {
Customer::updateOrCreate(
['lan_id' => $participant['lan_id']],
[
'lan_id' => $participant['lan_id'],
'name' => $participant['first_name'] . " " . $participant['surname'],
'guardian_name' => "Anna",
]
);
}
foreach ($response_data['volunteers'] as $volunteer) {
Customer::updateOrCreate(
['lan_id' => $volunteer['lan_id']],
[
'lan_id' => $volunteer['lan_id'],
'name' => $volunteer['first_name'] . " " . $volunteer['surname'],
'guardian_name' => "Anna",
]
);
}
if($latestVersionParticipant->version < $versions['participants']) {
Tableversion::create([
'table' => 'participants',
'version' =>$latestVersionParticipant->version + 1,
]);
}
if($latestVersionVolunteer->version < $versions['volunteers']) {
Tableversion::create([
'table' => 'volunteers',
'version' =>$latestVersionVolunteer->version + 1,
]);
}
}
})->everyMinute();

View file

@ -5,6 +5,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\CustomerGroupController;
Route::get('/', function () {
return Inertia::render('Auth/Login');
@ -12,6 +13,7 @@ Route::get('/', function () {
Route::get('/dashboard', [CustomerController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/customer/{id}', [CustomerController::class, 'show'])->middleware(['auth', 'verified']);
Route::get('/customer-groups', [CustomerGroupController::class, 'index'])->middleware(['auth', 'verified'])->name('customer-groups');
Route::get('/form', function () {
return Inertia::render('Form');