feat: rename invoice_number to order_number and refactor order management components and logic
This commit is contained in:
parent
c07f1bd3bb
commit
cf0d60f74b
@ -57,9 +57,16 @@ public function store(OrderRequest $request): RedirectResponse
|
||||
return $purchasePrice * $item['qty'];
|
||||
});
|
||||
|
||||
$lastOrder = Order::whereDate('created_at', now()->format('Y-m-d'))
|
||||
->lockForUpdate()
|
||||
->count();
|
||||
|
||||
$sequence = str_pad($lastOrder + 1, 3, '0', STR_PAD_LEFT);
|
||||
$orderNumber = 'ORD-'.now()->format('Ymd').'-'.$sequence;
|
||||
|
||||
$order = Order::create([
|
||||
'user_id' => auth()->id(),
|
||||
'invoice_number' => $validated['invoice_number'] ?? 'INV-'.now()->format('YmdHis').'-'.strtoupper(fake()->bothify('??##')),
|
||||
'order_number' => $orderNumber,
|
||||
'customer_name' => $validated['customer_name'],
|
||||
'cogs' => $cogs,
|
||||
'discount' => $validated['discount'],
|
||||
|
||||
@ -28,7 +28,6 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'invoice_number' => ['nullable', 'string', 'max:30'],
|
||||
'customer_name' => ['required', 'string', 'max:100'],
|
||||
'discount' => ['required', 'integer', 'min:0'],
|
||||
'payment' => ['required', 'integer', 'min:0'],
|
||||
|
||||
@ -77,7 +77,7 @@ public function items(): HasMany
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['invoice_number', 'customer_name', 'cogs', 'discount', 'payment', 'total', 'payment_method', 'order_status', 'order_channel'])
|
||||
->logOnly(['order_number', 'customer_name', 'cogs', 'discount', 'payment', 'total', 'payment_method', 'order_status', 'order_channel'])
|
||||
->logOnlyDirty()
|
||||
->useLogName('Pesanan');
|
||||
}
|
||||
@ -93,7 +93,7 @@ public function tapActivity(Activity $activity, string $eventName)
|
||||
|
||||
if (isset($activity->properties['attributes'])) {
|
||||
$attributeMap = [
|
||||
'invoice_number' => 'Nomor Invoice',
|
||||
'order_number' => 'Nomor Order',
|
||||
'customer_name' => 'Nama Pelanggan',
|
||||
'cogs' => 'Modal',
|
||||
'discount' => 'Diskon',
|
||||
|
||||
@ -24,10 +24,15 @@ class OrderFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
$date = $this->faker->dateTimeBetween('-1 year', 'now');
|
||||
$formattedDate = $date->format('Y-m-d');
|
||||
|
||||
$countToday = Order::whereDate('created_at', $formattedDate)->count();
|
||||
|
||||
$sequence = str_pad($countToday + 1, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
return [
|
||||
'user_id' => User::inRandomOrder()->first()?->id ?? User::factory(),
|
||||
'invoice_number' => 'INV-'.$date->format('YmdHis').'-'.strtoupper($this->faker->bothify('??##')),
|
||||
'order_number' => 'ORD-'.$date->format('YmdHi').'-'.$sequence,
|
||||
'customer_name' => $this->faker->name(),
|
||||
'cogs' => 0,
|
||||
'discount' => 0,
|
||||
|
||||
@ -17,7 +17,7 @@ public function up(): void
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('invoice_number', 30);
|
||||
$table->string('order_number', 30);
|
||||
$table->string('customer_name', 100);
|
||||
$table->unsignedInteger('cogs');
|
||||
$table->unsignedInteger('discount');
|
||||
|
||||
@ -63,7 +63,6 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
|
||||
}, [products]);
|
||||
|
||||
const { data, setData, post, processing, errors, transform } = useForm({
|
||||
invoice_number: '',
|
||||
customer_name: '',
|
||||
discount: 0,
|
||||
payment: 0,
|
||||
|
||||
@ -59,7 +59,6 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
|
||||
}, [products]);
|
||||
|
||||
const { data, setData, patch, processing, errors } = useForm({
|
||||
invoice_number: order.invoice_number,
|
||||
customer_name: order.customer_name,
|
||||
discount: order.discount,
|
||||
payment: order.payment,
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { format } from 'date-fns';
|
||||
import { id } from 'date-fns/locale';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { usePrinter } from '@/hooks/use-printer';
|
||||
import { EscPosEncoder } from '@/lib/esc-pos-encoder';
|
||||
import { formatCurrency } from '@/lib/formatters';
|
||||
import type { Order } from '@/types/order';
|
||||
import { format } from 'date-fns';
|
||||
import { id } from 'date-fns/locale';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function useOrderPrint() {
|
||||
const {
|
||||
@ -40,7 +40,7 @@ export function useOrderPrint() {
|
||||
.bold(false)
|
||||
.line(line)
|
||||
.align(0)
|
||||
.line(`No. Invoice : ${order.invoice_number}`)
|
||||
.line(`No. Order : ${order.order_number}`)
|
||||
.line(`Tanggal : ${format(new Date(order.created_at), 'dd MMMM yyyy HH:mm', { locale: id })}`)
|
||||
.line(`Pelanggan : ${order.customer_name}`)
|
||||
.line(line);
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { usePermission } from '@/hooks/use-permission';
|
||||
import * as orderRoutes from '@/routes/order';
|
||||
import type { Order } from '@/types/order';
|
||||
import { useOrderIndex } from './hooks/use-order-index';
|
||||
import { useOrderPrint } from './hooks/use-order-print';
|
||||
import { getColumns } from './partials/columns';
|
||||
import { PrinterSettings } from './partials/printer-settings';
|
||||
import { usePermission } from '@/hooks/use-permission';
|
||||
|
||||
export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
const { hasPermission } = usePermission();
|
||||
@ -38,11 +38,14 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
connectUsb,
|
||||
disconnect,
|
||||
setPaperSize,
|
||||
handlePrint
|
||||
handlePrint,
|
||||
} = useOrderPrint();
|
||||
|
||||
const columns = getColumns({ onDelete, onPrint: handlePrint }).filter(
|
||||
(col) => col.id !== 'actions' || hasPermission('Edit:Order') || hasPermission('Delete:Order')
|
||||
(col) =>
|
||||
col.id !== 'actions' ||
|
||||
hasPermission('Edit:Order') ||
|
||||
hasPermission('Delete:Order'),
|
||||
);
|
||||
|
||||
return (
|
||||
@ -51,7 +54,9 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">Pesanan</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
||||
Pesanan
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PrinterSettings
|
||||
@ -75,7 +80,7 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
|
||||
<Card className="overflow-hidden border-none bg-card/50 p-5 shadow-lg backdrop-blur-sm">
|
||||
<CardContent className="p-0">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
@ -109,7 +114,9 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
title="Hapus pesanan?"
|
||||
description={
|
||||
<>
|
||||
Tindakan ini tidak dapat dibatalkan. Pesanan <strong>{orderToDelete?.invoice_number}</strong> akan dihapus secara permanen dan stok akan dikembalikan.
|
||||
Tindakan ini tidak dapat dibatalkan. Pesanan{' '}
|
||||
<strong>{orderToDelete?.order_number}</strong> akan
|
||||
dihapus secara permanen dan stok akan dikembalikan.
|
||||
</>
|
||||
}
|
||||
onDelete={confirmDelete}
|
||||
@ -122,7 +129,9 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||
title={`Hapus ${rowsToDelete.length} pesanan?`}
|
||||
description={
|
||||
<>
|
||||
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> item yang terpilih akan dihapus secara permanen.
|
||||
Tindakan ini tidak dapat dibatalkan.{' '}
|
||||
<strong>{rowsToDelete.length}</strong> item yang
|
||||
terpilih akan dihapus secara permanen.
|
||||
</>
|
||||
}
|
||||
onDelete={confirmBulkDelete}
|
||||
@ -135,6 +144,6 @@ OrderIndex.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Kelola',
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -1,54 +1,68 @@
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { usePermission } from '@/hooks/use-permission';
|
||||
import * as orderRoutes from '@/routes/order';
|
||||
import type { Order } from '@/types/order';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { id } from 'date-fns/locale';
|
||||
import { Pencil, Printer, Trash2 } from 'lucide-react';
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import * as orderRoutes from '@/routes/order';
|
||||
import type { Order } from '@/types/order';
|
||||
|
||||
interface ColumnProps {
|
||||
onDelete: (order: Order) => void;
|
||||
onPrint: (order: Order) => void;
|
||||
}
|
||||
|
||||
export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>[] => [
|
||||
export const getColumns = ({
|
||||
onDelete,
|
||||
onPrint,
|
||||
}: ColumnProps): ColumnDef<Order>[] => [
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
accessorKey: 'order_number',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="No. Invoice" />
|
||||
<DataTableColumnHeader column={column} title="No. Order" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{row.original.invoice_number}</span>
|
||||
<span className="font-medium">{row.original.order_number}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{format(new Date(row.original.created_at), 'dd MMM yyyy HH:mm', { locale: id })}
|
||||
{format(
|
||||
new Date(row.original.created_at),
|
||||
'dd MMM yyyy HH:mm',
|
||||
{ locale: id },
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
meta: { title: "No. Invoice" },
|
||||
meta: { title: 'No. Order' },
|
||||
},
|
||||
{
|
||||
accessorKey: "customer_name",
|
||||
accessorKey: 'customer_name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Pelanggan" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span>{row.original.customer_name}</span>
|
||||
<Badge variant="outline" className="w-fit text-[10px] px-1 py-0 h-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-4 w-fit px-1 py-0 text-[10px]"
|
||||
>
|
||||
{row.original.order_channel}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
meta: { title: "Pelanggan" },
|
||||
meta: { title: 'Pelanggan' },
|
||||
},
|
||||
{
|
||||
accessorKey: "items",
|
||||
accessorKey: 'items',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Item" />
|
||||
),
|
||||
@ -56,27 +70,37 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
const items = row.original.items;
|
||||
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return <span className="text-muted-foreground text-xs italic">-</span>;
|
||||
return (
|
||||
<span className="text-xs text-muted-foreground italic">
|
||||
-
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{items.slice(0, 2).map((item, i) => (
|
||||
<div key={i} className="text-xs">
|
||||
<span className="text-muted-foreground">{item.product?.name}:</span>{' '}
|
||||
<span className="font-medium text-primary">{item.qty} x {item.price_formatted}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{item.product?.name}:
|
||||
</span>{' '}
|
||||
<span className="font-medium text-primary">
|
||||
{item.qty} x {item.price_formatted}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{items.length > 2 && (
|
||||
<span className="text-[10px] text-muted-foreground italic">+{items.length - 2} item lainnya...</span>
|
||||
<span className="text-[10px] text-muted-foreground italic">
|
||||
+{items.length - 2} item lainnya...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { title: "Item" },
|
||||
meta: { title: 'Item' },
|
||||
},
|
||||
{
|
||||
accessorKey: "total",
|
||||
accessorKey: 'total',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Total" />
|
||||
),
|
||||
@ -85,20 +109,27 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
<span className="font-bold text-primary">
|
||||
{row.original.total_formatted}
|
||||
</span>
|
||||
<Badge className="text-[10px] h-4 py-0" variant={
|
||||
row.original.order_status === 'delivered' ? 'default' :
|
||||
row.original.order_status === 'pending' ? 'secondary' :
|
||||
row.original.order_status === 'cancelled' ? 'destructive' : 'outline'
|
||||
}>
|
||||
<Badge
|
||||
className="h-4 py-0 text-[10px]"
|
||||
variant={
|
||||
row.original.order_status === 'delivered'
|
||||
? 'default'
|
||||
: row.original.order_status === 'pending'
|
||||
? 'secondary'
|
||||
: row.original.order_status === 'cancelled'
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
{row.original.order_status}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
meta: { title: "Total" },
|
||||
meta: { title: 'Total' },
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Aksi",
|
||||
id: 'actions',
|
||||
header: 'Aksi',
|
||||
cell: ({ row }) => {
|
||||
const order = row.original;
|
||||
const { hasPermission } = usePermission();
|
||||
@ -107,7 +138,12 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className='text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20' onClick={() => onPrint(order)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-blue-600 hover:bg-blue-50 hover:text-blue-700 dark:hover:bg-blue-950/20"
|
||||
onClick={() => onPrint(order)}
|
||||
>
|
||||
<Printer className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@ -119,7 +155,11 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={orderRoutes.edit(order.id).url}>
|
||||
<Button variant="ghost" size="icon" className='text-yellow-600 hover:text-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-950/20'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-yellow-600 hover:bg-yellow-50 hover:text-yellow-700 dark:hover:bg-yellow-950/20"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@ -132,7 +172,12 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
{hasPermission('Delete:Order') && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className='text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/20' onClick={() => onDelete(order)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-red-600 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/20"
|
||||
onClick={() => onDelete(order)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@ -144,6 +189,6 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { title: "Aksi" },
|
||||
meta: { title: 'Aksi' },
|
||||
},
|
||||
];
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Product } from "./product";
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
invoice_number: string;
|
||||
order_number: string;
|
||||
customer_name: string;
|
||||
cogs: number;
|
||||
discount: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user