refactor: restrict marketing role permissions and add employee advances and payroll access
This commit is contained in:
parent
2dab1efeae
commit
69d2212d64
@ -201,8 +201,6 @@ public function permissions(): array
|
||||
self::MARKETING => [
|
||||
Permission::DASHBOARD_VIEW,
|
||||
|
||||
Permission::EMPLOYEES_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
Permission::ATTENDANCES_CREATE,
|
||||
|
||||
@ -212,28 +210,20 @@ public function permissions(): array
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
Permission::ORDERS_CREATE,
|
||||
Permission::ORDERS_UPDATE,
|
||||
Permission::ORDERS_SEND,
|
||||
Permission::ORDERS_COMPLETE,
|
||||
Permission::ORDERS_CANCEL,
|
||||
|
||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
||||
|
||||
Permission::PAYROLL_VIEW,
|
||||
],
|
||||
|
||||
self::NON_OPERATOR => [
|
||||
|
||||
@ -41,6 +41,7 @@ public function create(Request $request): Response
|
||||
|
||||
return Inertia::render('admin/manage/orders/Create', [
|
||||
'customers' => $this->orderService->customerOptions(),
|
||||
'marketings' => $this->orderService->marketingOptions(),
|
||||
'catalog' => $this->orderService->catalogItems(user: $user),
|
||||
'channels' => OrderChannel::selectOptions(),
|
||||
'storePriceTypes' => $this->orderService->storePriceTypeOptions(),
|
||||
@ -68,6 +69,7 @@ public function edit(Order $order): Response|RedirectResponse
|
||||
return Inertia::render('admin/manage/orders/Edit', [
|
||||
'order' => $this->orderService->findForEdit($order),
|
||||
'customers' => $this->orderService->customerOptions(),
|
||||
'marketings' => $this->orderService->marketingOptions(),
|
||||
'catalog' => $this->orderService->catalogItems($order),
|
||||
'channels' => OrderChannel::selectOptions(),
|
||||
'storePriceTypes' => $this->orderService->storePriceTypeOptions(),
|
||||
|
||||
@ -28,6 +28,7 @@ public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'customer_id' => ['nullable', 'integer', Rule::exists('customers', 'id')->whereNull('deleted_at')],
|
||||
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')->whereNull('deleted_at')],
|
||||
'channel' => ['required', Rule::enum(OrderChannel::class)],
|
||||
'price_type' => ['required', Rule::enum(PriceType::class)],
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
@ -54,6 +55,7 @@ public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'customer_id' => 'pelanggan',
|
||||
'marketing_id' => 'marketing',
|
||||
'channel' => 'channel',
|
||||
'price_type' => 'tipe harga',
|
||||
'discount' => 'diskon',
|
||||
|
||||
@ -54,6 +54,11 @@ public function createdBy(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'created_by_id');
|
||||
}
|
||||
|
||||
public function marketing(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'marketing_id');
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
|
||||
@ -41,6 +41,7 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
|
||||
'items.productVariant.product:id,name',
|
||||
'items.productVariant:id,product_id,name',
|
||||
])
|
||||
->when($user->hasRole('marketing'), fn (Builder $query) => $query->where('marketing_id', $user->id))
|
||||
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
||||
$search = $tableQuery['search'];
|
||||
$query->where(function (Builder $query) use ($search): void {
|
||||
@ -87,6 +88,24 @@ public function customerOptions(): array
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{value: int, label: string}>
|
||||
*/
|
||||
public function marketingOptions(): array
|
||||
{
|
||||
return User::query()
|
||||
->active()
|
||||
->whereHas('roles', fn ($q) => $q->where('name', 'marketing'))
|
||||
->with('profile:user_id,full_name')
|
||||
->orderBy('username')
|
||||
->get(['id', 'username'])
|
||||
->map(fn (User $user) => [
|
||||
'value' => $user->id,
|
||||
'label' => $user->profile?->full_name ?? $user->username,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{value: string, label: string}>
|
||||
*/
|
||||
@ -301,6 +320,7 @@ public function create(array $validated, User $user): Order
|
||||
|
||||
$order = Order::create([
|
||||
'customer_id' => $validated['customer_id'] ?? null,
|
||||
'marketing_id' => $validated['marketing_id'] ?? null,
|
||||
'channel' => $channel,
|
||||
'price_type' => $priceType,
|
||||
'status' => OrderStatus::PENDING,
|
||||
@ -374,6 +394,7 @@ public function update(Order $order, array $validated): void
|
||||
$channel = OrderChannel::from($validated['channel']);
|
||||
|
||||
$order->customer_id = $validated['customer_id'] ?? null;
|
||||
$order->marketing_id = $validated['marketing_id'] ?? null;
|
||||
$order->channel = $channel;
|
||||
$order->price_type = $priceType;
|
||||
$order->subtotal = $subtotal;
|
||||
|
||||
@ -15,6 +15,7 @@ public function up(): void
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('marketing_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
$table->string('order_number', 30)->unique();
|
||||
$table->enum('channel', array_column(OrderChannel::cases(), 'value'));
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { useForm, usePage } from '@inertiajs/vue3';
|
||||
import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import type { Auth } from '@/types/auth';
|
||||
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
|
||||
import { NumberInput } from '@/components/form/number-input';
|
||||
@ -42,11 +43,13 @@ import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
|
||||
|
||||
const props = defineProps<{
|
||||
customers: SelectOption[];
|
||||
marketings: SelectOption[];
|
||||
catalog: OrderCatalogItem[];
|
||||
channels: EnumOption[];
|
||||
storePriceTypes: EnumOption[];
|
||||
initialData?: {
|
||||
customer_id: string;
|
||||
marketing_id: string;
|
||||
channel: string;
|
||||
price_type: string;
|
||||
discount: string;
|
||||
@ -61,11 +64,23 @@ const props = defineProps<{
|
||||
|
||||
const isCreateMode = computed(() => props.method === 'post');
|
||||
|
||||
const page = usePage();
|
||||
const authUser = computed(() => (page.props.auth as Auth).user);
|
||||
const isMarketingUser = computed(() =>
|
||||
authUser.value?.roles?.includes('marketing') ?? false,
|
||||
);
|
||||
|
||||
const search = ref('');
|
||||
const cart = ref<OrderCartItem[]>([]);
|
||||
|
||||
// Auto-fill marketing_id with logged-in user's id if they are marketing role
|
||||
const defaultMarketingId = computed(() =>
|
||||
isMarketingUser.value && authUser.value ? String(authUser.value.id) : '',
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
customer_id: '',
|
||||
marketing_id: defaultMarketingId.value || 'none',
|
||||
channel: 'store',
|
||||
price_type: 'ecer',
|
||||
discount: '',
|
||||
@ -80,6 +95,7 @@ function populateForm() {
|
||||
}
|
||||
|
||||
form.customer_id = props.initialData.customer_id;
|
||||
form.marketing_id = props.initialData.marketing_id || 'none';
|
||||
form.channel = props.initialData.channel;
|
||||
form.price_type = props.initialData.price_type;
|
||||
form.discount = props.initialData.discount;
|
||||
@ -323,6 +339,12 @@ function buildFormData(): FormData {
|
||||
formData.append('customer_id', form.customer_id);
|
||||
}
|
||||
|
||||
if (form.marketing_id && form.marketing_id !== 'none') {
|
||||
formData.append('marketing_id', form.marketing_id);
|
||||
} else if (isMarketingUser.value && authUser.value) {
|
||||
formData.append('marketing_id', String(authUser.value.id));
|
||||
}
|
||||
|
||||
formData.append('channel', form.channel);
|
||||
formData.append('price_type', form.price_type);
|
||||
formData.append('discount', parseRupiah(form.discount));
|
||||
@ -502,6 +524,30 @@ function submit() {
|
||||
<FieldError :errors="formErrors(form, 'customer_id')" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="marketing">Marketing</FieldLabel>
|
||||
<Select v-model="form.marketing_id" :disabled="isMarketingUser">
|
||||
<SelectTrigger id="marketing" class="w-full">
|
||||
<SelectValue placeholder="Pilih marketing (opsional)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="none">
|
||||
— Tidak ada —
|
||||
</SelectItem>
|
||||
<SelectItem v-for="marketing in marketings" :key="marketing.value"
|
||||
:value="String(marketing.value)">
|
||||
{{ marketing.label }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p v-if="isMarketingUser" class="text-xs text-muted-foreground">
|
||||
Otomatis tercatat atas nama Anda.
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'marketing_id')" />
|
||||
</Field>
|
||||
|
||||
<div v-if="cart.length === 0"
|
||||
class="rounded-lg border border-dashed text-center text-sm text-muted-foreground">
|
||||
<Empty>
|
||||
|
||||
@ -8,6 +8,7 @@ import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '
|
||||
|
||||
defineProps<{
|
||||
customers: SelectOption[];
|
||||
marketings: SelectOption[];
|
||||
catalog: OrderCatalogItem[];
|
||||
channels: EnumOption[];
|
||||
storePriceTypes: EnumOption[];
|
||||
@ -36,6 +37,7 @@ defineProps<{
|
||||
|
||||
<OrderPosForm
|
||||
:customers="customers"
|
||||
:marketings="marketings"
|
||||
:catalog="catalog"
|
||||
:channels="channels"
|
||||
:store-price-types="storePriceTypes"
|
||||
|
||||
@ -10,6 +10,7 @@ import type { EnumOption, OrderCatalogItem, OrderEditItem, SelectOption } from '
|
||||
const props = defineProps<{
|
||||
order: OrderEditItem;
|
||||
customers: SelectOption[];
|
||||
marketings: SelectOption[];
|
||||
catalog: OrderCatalogItem[];
|
||||
channels: EnumOption[];
|
||||
storePriceTypes: EnumOption[];
|
||||
@ -17,6 +18,7 @@ const props = defineProps<{
|
||||
|
||||
const initialData = computed(() => ({
|
||||
customer_id: props.order.customer_id ? String(props.order.customer_id) : '',
|
||||
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
|
||||
channel: props.order.channel,
|
||||
price_type: props.order.price_type,
|
||||
discount: String(props.order.discount),
|
||||
@ -56,6 +58,7 @@ const initialData = computed(() => ({
|
||||
|
||||
<OrderPosForm
|
||||
:customers="customers"
|
||||
:marketings="marketings"
|
||||
:catalog="catalog"
|
||||
:channels="channels"
|
||||
:store-price-types="storePriceTypes"
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import OrderGroupedTable from '@/components/admin/manage/orders/OrderGroupedTable.vue';
|
||||
import ThermalPrinterConnectButton from '@/components/admin/manage/orders/ThermalPrinterConnectButton.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import {
|
||||
useDataTableQuery,
|
||||
useDataTableQuerySync,
|
||||
} from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { PaginatedOrders } from '@/types/order';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
orders: PaginatedOrders;
|
||||
@ -37,11 +40,11 @@ const tablePagination = computed(() => ({
|
||||
total: props.orders.total,
|
||||
}));
|
||||
|
||||
const firstItem = computed(() => (
|
||||
const firstItem = computed(() =>
|
||||
props.orders.data.length > 0
|
||||
? (props.orders.current_page - 1) * props.orders.per_page + 1
|
||||
: 0
|
||||
));
|
||||
: 0,
|
||||
);
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
@ -59,17 +62,20 @@ watch(
|
||||
<Head title="Pesanan" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div
|
||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Pesanan
|
||||
</h2>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Pesanan</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center">
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-2 self-start sm:self-center"
|
||||
v-if="can('orders.create')"
|
||||
>
|
||||
<ThermalPrinterConnectButton />
|
||||
|
||||
<Button v-if="can('orders.create')" as-child>
|
||||
<Button as-child>
|
||||
<Link href="/admin/manage/orders/create">
|
||||
<Plus class="size-4" />
|
||||
Tambah
|
||||
|
||||
@ -76,6 +76,7 @@ export type OrderEditItem = {
|
||||
id: number;
|
||||
order_number: string;
|
||||
customer_id: number | null;
|
||||
marketing_id: number | null;
|
||||
channel: string;
|
||||
price_type: string;
|
||||
status: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user