feat: add complete action for cuttings with notification and update route
This commit is contained in:
parent
7c160cb571
commit
703cdb8b8b
@ -95,6 +95,15 @@ public function destroy(Cutting $cutting): RedirectResponse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function complete(Cutting $cutting): RedirectResponse
|
||||||
|
{
|
||||||
|
return $this->handleAction(
|
||||||
|
fn () => $this->service->complete($cutting),
|
||||||
|
'Cutting berhasil diselesaikan.',
|
||||||
|
'admin.manage.cuttings.index'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function materials(Cutting $cutting): JsonResponse
|
public function materials(Cutting $cutting): JsonResponse
|
||||||
{
|
{
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Manage;
|
namespace App\Services\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\CuttingStatus;
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Models\Cutting;
|
use App\Models\Cutting;
|
||||||
use App\Models\CuttingMaterial;
|
use App\Models\CuttingMaterial;
|
||||||
@ -500,4 +501,18 @@ public function destroy(Cutting $cutting): bool
|
|||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function complete(Cutting $cutting): Cutting
|
||||||
|
{
|
||||||
|
$cutting->update(['status' => CuttingStatus::COMPLETED]);
|
||||||
|
|
||||||
|
NotificationService::notify(
|
||||||
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||||
|
title: 'Cutting Selesai',
|
||||||
|
body: 'Cutting berhasil diselesaikan oleh '.auth()->user()->full_name.'.',
|
||||||
|
url: route('admin.manage.cuttings.index'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $cutting;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { Briefcase, ChevronDown, Copy, MessageCircle, Pencil, Share2, Trash2 } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { RowActions } from '@/components/data-display';
|
import { RowActions } from '@/components/data-display';
|
||||||
import { ImagePreviewButton } from '@/components/dialogs';
|
import { ImagePreviewButton } from '@/components/dialogs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -13,6 +11,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
|||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { share as cuttingShare } from '@/routes/admin/manage/cuttings';
|
import { share as cuttingShare } from '@/routes/admin/manage/cuttings';
|
||||||
|
import { ChevronDown, Pencil, ScissorsIcon, Share2, Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import type { Cutting } from './columns';
|
import type { Cutting } from './columns';
|
||||||
|
|
||||||
function generateShareLink(cuttingId: number): string {
|
function generateShareLink(cuttingId: number): string {
|
||||||
@ -90,6 +90,7 @@ export type CuttingCardRowParams = {
|
|||||||
index: number;
|
index: number;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
|
onComplete: (cutting: Cutting) => void;
|
||||||
onEdit: (cutting: Cutting) => void;
|
onEdit: (cutting: Cutting) => void;
|
||||||
onDelete: (cutting: Cutting) => void;
|
onDelete: (cutting: Cutting) => void;
|
||||||
};
|
};
|
||||||
@ -99,12 +100,12 @@ export function CuttingCardRow({
|
|||||||
index,
|
index,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
|
onComplete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: CuttingCardRowParams) {
|
}: CuttingCardRowParams) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const [shareOpen, setShareOpen] = useState(false);
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const materialsCount = cutting.materials_count ?? 0;
|
const materialsCount = cutting.materials_count ?? 0;
|
||||||
const totalUsage = cutting.total_usage ?? 0;
|
const totalUsage = cutting.total_usage ?? 0;
|
||||||
const productName = cutting.product_name ?? '-';
|
const productName = cutting.product_name ?? '-';
|
||||||
@ -254,6 +255,12 @@ export function CuttingCardRow({
|
|||||||
{(can('cuttings.update') || can('cuttings.delete')) && (
|
{(can('cuttings.update') || can('cuttings.delete')) && (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Selesaikan',
|
||||||
|
icon: <ScissorsIcon className="h-4 w-4" />,
|
||||||
|
show: can('cuttings.complete') && cutting.status === 'in_progress',
|
||||||
|
onClick: () => onComplete(cutting),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
index as cuttingIndex,
|
index as cuttingIndex,
|
||||||
edit as cuttingEdit,
|
edit as cuttingEdit,
|
||||||
materials as cuttingMaterials,
|
materials as cuttingMaterials,
|
||||||
|
complete as cuttingComplete,
|
||||||
} from '@/routes/admin/manage/cuttings';
|
} from '@/routes/admin/manage/cuttings';
|
||||||
import type { Cutting, CuttingMaterialDetail, CuttingCombination } from './columns';
|
import type { Cutting, CuttingMaterialDetail, CuttingCombination } from './columns';
|
||||||
import { CuttingCardRow } from './cutting-card';
|
import { CuttingCardRow } from './cutting-card';
|
||||||
@ -60,6 +61,7 @@ export default function CuttingIndex({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const [deleting, setDeleting] = useState<Cutting | null>(null);
|
const [deleting, setDeleting] = useState<Cutting | null>(null);
|
||||||
|
const [completing, setCompleting] = useState<Cutting | null>(null);
|
||||||
const [loadedMaterials, setLoadedMaterials] = useState<Record<number, CuttingMaterialDetail[]>>({});
|
const [loadedMaterials, setLoadedMaterials] = useState<Record<number, CuttingMaterialDetail[]>>({});
|
||||||
const [loadedCombinations, setLoadedCombinations] = useState<Record<number, CuttingCombination[]>>({});
|
const [loadedCombinations, setLoadedCombinations] = useState<Record<number, CuttingCombination[]>>({});
|
||||||
const [loadingMaterials, setLoadingMaterials] = useState<Record<number, boolean>>({});
|
const [loadingMaterials, setLoadingMaterials] = useState<Record<number, boolean>>({});
|
||||||
@ -100,6 +102,16 @@ export default function CuttingIndex({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleComplete() {
|
||||||
|
if (!completing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.patch(cuttingComplete.url(completing.id), {}, {
|
||||||
|
onSuccess: () => setCompleting(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const selectedProductName = useMemo(
|
const selectedProductName = useMemo(
|
||||||
() =>
|
() =>
|
||||||
filterOptions.productNames.find(
|
filterOptions.productNames.find(
|
||||||
@ -261,6 +273,7 @@ export default function CuttingIndex({
|
|||||||
}
|
}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
|
onComplete={(c) => setCompleting(c)}
|
||||||
onEdit={(c) => {
|
onEdit={(c) => {
|
||||||
router.visit(cuttingEdit.url(c.id));
|
router.visit(cuttingEdit.url(c.id));
|
||||||
}}
|
}}
|
||||||
@ -290,6 +303,21 @@ export default function CuttingIndex({
|
|||||||
}
|
}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
target={completing}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCompleting(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Selesaikan Cutting"
|
||||||
|
description={(cutting) =>
|
||||||
|
`Apakah Anda yakin ingin menyelesaikan cutting "${cutting.cutting_results?.[0]?.product_name ?? '-'}"?`
|
||||||
|
}
|
||||||
|
confirmLabel="Selesaikan"
|
||||||
|
onConfirm={handleComplete}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -72,6 +72,7 @@
|
|||||||
|
|
||||||
Route::resource('cuttings', CuttingController::class)->middleware('permission:cuttings.view|cuttings.create|cuttings.update|cuttings.delete');
|
Route::resource('cuttings', CuttingController::class)->middleware('permission:cuttings.view|cuttings.create|cuttings.update|cuttings.delete');
|
||||||
Route::get('cuttings/{cutting}/materials', [CuttingController::class, 'materials'])->name('cuttings.materials')->middleware('permission:cuttings.view');
|
Route::get('cuttings/{cutting}/materials', [CuttingController::class, 'materials'])->name('cuttings.materials')->middleware('permission:cuttings.view');
|
||||||
|
Route::patch('cuttings/{cutting}/complete', [CuttingController::class, 'complete'])->name('cuttings.complete')->middleware('permission:cuttings.complete');
|
||||||
|
|
||||||
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
||||||
Route::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
Route::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user