feat: add cutting detail view and enhance cutting data handling in various components
This commit is contained in:
parent
5182dcc012
commit
a778e2f499
@ -42,6 +42,13 @@ public function create(): Response
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Cutting $cutting): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/manage/cutting/show', [
|
||||||
|
'cutting' => $this->service->getForShow($cutting),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function store(CuttingRequest $request): RedirectResponse
|
public function store(CuttingRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
|
|
||||||
#[Appends(['formatted_cost_per_unit', 'formatted_other_cost', 'formatted_sewing_cost', 'status_label', 'formatted_total_material_cost'])]
|
#[Appends(['formatted_cost_per_unit', 'formatted_other_cost', 'formatted_sewing_cost', 'status_label', 'formatted_total_material_cost', 'formatted_created_at'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class Cutting extends Model implements HasMedia
|
class Cutting extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
@ -70,6 +70,13 @@ protected function formattedTotalMaterialCost(): Attribute
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedCreatedAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->created_at?->translatedFormat('l, d F Y'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[Scope]
|
#[Scope]
|
||||||
protected function cancelled(Builder $query): void
|
protected function cancelled(Builder $query): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,9 +29,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->with([
|
->with([
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
'cuttingResults:id,cutting_id,product_name,cutting_result,sample,original_outside_sample',
|
'cuttingResults:id,cutting_id,product_name,cutting_result',
|
||||||
'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id',
|
'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id',
|
||||||
'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant',
|
||||||
'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit',
|
'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
'cuttingMaterialCombinations:id,cutting_id,material_result',
|
'cuttingMaterialCombinations:id,cutting_id,material_result',
|
||||||
])
|
])
|
||||||
@ -80,6 +80,89 @@ public function getProductNames(): Collection
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getForShow(Cutting $cutting): array
|
||||||
|
{
|
||||||
|
$cutting->load([
|
||||||
|
'createdBy:id',
|
||||||
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
|
'cuttingResults:id,cutting_id,product_name,cutting_result,sample,original_outside_sample',
|
||||||
|
'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id',
|
||||||
|
'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant',
|
||||||
|
'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
|
'cuttingMaterialCombinations:id,cutting_id,material_result',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cuttingMedia = $cutting->getFirstMedia('images');
|
||||||
|
$cutting->photo_url = $cuttingMedia
|
||||||
|
? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath())
|
||||||
|
: null;
|
||||||
|
$cutting->photo_conversion_url = $cuttingMedia
|
||||||
|
? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$cutting->cuttingMaterials->each(function (CuttingMaterial $material) {
|
||||||
|
$media = $material->rawMaterialPrice?->getFirstMedia('images');
|
||||||
|
if ($material->rawMaterialPrice) {
|
||||||
|
$material->rawMaterialPrice->photo_url = $media
|
||||||
|
? $this->s3Service->getTemporaryUrl($media->getPath())
|
||||||
|
: null;
|
||||||
|
$material->rawMaterialPrice->photo_conversion_url = $media
|
||||||
|
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $cutting->id,
|
||||||
|
'status' => $cutting->status->value,
|
||||||
|
'status_label' => $cutting->status_label,
|
||||||
|
'description' => $cutting->description,
|
||||||
|
'total_material_cost' => $cutting->total_material_cost,
|
||||||
|
'formatted_total_material_cost' => $cutting->formatted_total_material_cost,
|
||||||
|
'cost_per_unit' => $cutting->cost_per_unit,
|
||||||
|
'formatted_cost_per_unit' => $cutting->formatted_cost_per_unit,
|
||||||
|
'created_at' => $cutting->created_at,
|
||||||
|
'formatted_created_at' => $cutting->formatted_created_at,
|
||||||
|
'photo_url' => $cutting->photo_url,
|
||||||
|
'photo_conversion_url' => $cutting->photo_conversion_url,
|
||||||
|
'created_by' => [
|
||||||
|
'id' => $cutting->createdBy->id,
|
||||||
|
'user_profile' => [
|
||||||
|
'full_name' => $cutting->createdBy->userProfile->full_name ?? '-',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'cutting_results' => $cutting->cuttingResults->map(fn ($r) => [
|
||||||
|
'id' => $r->id,
|
||||||
|
'product_name' => $r->product_name,
|
||||||
|
'cutting_result' => $r->cutting_result,
|
||||||
|
'sample' => $r->sample,
|
||||||
|
'original_outside_sample' => $r->original_outside_sample,
|
||||||
|
]),
|
||||||
|
'cutting_materials' => $cutting->cuttingMaterials->map(fn ($m) => [
|
||||||
|
'id' => $m->id,
|
||||||
|
'material_usage' => $m->material_usage,
|
||||||
|
'material_result' => $m->material_result,
|
||||||
|
'combination_id' => $m->combination_id,
|
||||||
|
'raw_material_price' => [
|
||||||
|
'id' => $m->rawMaterialPrice->id,
|
||||||
|
'variant' => $m->rawMaterialPrice->variant,
|
||||||
|
'price' => $m->rawMaterialPrice->price,
|
||||||
|
'photo_url' => $m->rawMaterialPrice->photo_url,
|
||||||
|
'photo_conversion_url' => $m->rawMaterialPrice->photo_conversion_url,
|
||||||
|
'raw_material' => [
|
||||||
|
'id' => $m->rawMaterialPrice->rawMaterial->id,
|
||||||
|
'name' => $m->rawMaterialPrice->rawMaterial->name,
|
||||||
|
'unit' => $m->rawMaterialPrice->rawMaterial->unit,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'cutting_material_combinations' => $cutting->cuttingMaterialCombinations->map(fn ($c) => [
|
||||||
|
'id' => $c->id,
|
||||||
|
'material_result' => $c->material_result,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getForEdit(Cutting $cutting): array
|
public function getForEdit(Cutting $cutting): array
|
||||||
{
|
{
|
||||||
$cutting->load([
|
$cutting->load([
|
||||||
|
|||||||
@ -24,7 +24,7 @@ createInertiaApp({
|
|||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
layout: (name) => {
|
layout: (name) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case name === 'welcome':
|
case name === 'admin/manage/cutting/show':
|
||||||
return null;
|
return null;
|
||||||
case name.startsWith('auth/'):
|
case name.startsWith('auth/'):
|
||||||
return AuthLayout;
|
return AuthLayout;
|
||||||
|
|||||||
@ -26,10 +26,13 @@ export type Cutting = {
|
|||||||
status: string;
|
status: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
total_material_cost: number | null;
|
total_material_cost: number | null;
|
||||||
|
formatted_total_material_cost: string | null;
|
||||||
cost_per_unit: number | null;
|
cost_per_unit: number | null;
|
||||||
|
formatted_cost_per_unit: string | null;
|
||||||
photo_url: string | null;
|
photo_url: string | null;
|
||||||
photo_conversion_url: string | null;
|
photo_conversion_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
formatted_created_at: string;
|
||||||
created_by: {
|
created_by: {
|
||||||
id: number;
|
id: number;
|
||||||
user_profile: {
|
user_profile: {
|
||||||
@ -46,8 +49,6 @@ export type Cutting = {
|
|||||||
raw_material_price: {
|
raw_material_price: {
|
||||||
id: number;
|
id: number;
|
||||||
variant: string;
|
variant: string;
|
||||||
price: number;
|
|
||||||
stock: number;
|
|
||||||
photo_url: string | null;
|
photo_url: string | null;
|
||||||
photo_conversion_url: string | null;
|
photo_conversion_url: string | null;
|
||||||
raw_material: {
|
raw_material: {
|
||||||
|
|||||||
@ -1,13 +1,90 @@
|
|||||||
import { ChevronDown, Pencil, Trash2 } from 'lucide-react';
|
|
||||||
import { ImagePreviewButton } from '@/components/dialogs';
|
|
||||||
import { RowActions } from '@/components/data-display';
|
import { RowActions } from '@/components/data-display';
|
||||||
|
import { ImagePreviewButton } from '@/components/dialogs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { show as cuttingShow } from '@/routes/admin/manage/cuttings';
|
||||||
|
import { Briefcase, ChevronDown, Copy, MessageCircle, Pencil, Share2, Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import type { Cutting } from './columns';
|
import type { Cutting } from './columns';
|
||||||
|
|
||||||
|
function generateShareLink(cuttingId: number): string {
|
||||||
|
return window.location.origin + cuttingShow.url(cuttingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWhatsappText(cutting: Cutting): string {
|
||||||
|
const result = cutting.cutting_results?.[0];
|
||||||
|
const items = cutting.cutting_materials ?? [];
|
||||||
|
const shareLink = generateShareLink(cutting.id);
|
||||||
|
|
||||||
|
let text = `*Cutting #${cutting.id}*\n`;
|
||||||
|
text += `Status: ${cutting.status === 'completed' ? 'Selesai' : cutting.status === 'cancelled' ? 'Dibatalkan' : 'Dikerjakan'}\n`;
|
||||||
|
text += `Tanggal: ${cutting.formatted_created_at}\n`;
|
||||||
|
text += `Oleh: ${cutting.created_by?.user_profile?.full_name ?? '-'}\n`;
|
||||||
|
|
||||||
|
if (cutting.description) {
|
||||||
|
text += `Deskripsi: ${cutting.description}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productNames = new Set<string>();
|
||||||
|
(cutting.cutting_results ?? []).forEach((r) => {
|
||||||
|
if (r.product_name) {
|
||||||
|
productNames.add(r.product_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (productNames.size > 0) {
|
||||||
|
text += `\nNama Produk: ${Array.from(productNames).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUsage = items.reduce(
|
||||||
|
(sum, item) => sum + Number(item.material_usage),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
text += `Total Pemakaian: ${formatNumber(totalUsage)}\n`;
|
||||||
|
|
||||||
|
if (result?.cutting_result) {
|
||||||
|
text += `Total Hasil Cutting: ${formatNumber(result.cutting_result)} pcs\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
text += `\nLihat detail lengkap:\n${shareLink}`;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWAWeb(text: string) {
|
||||||
|
const encoded = encodeURIComponent(text);
|
||||||
|
window.open(`https://wa.me/?text=${encoded}`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWAAndroid(text: string, pkg: string) {
|
||||||
|
const encoded = encodeURIComponent(text);
|
||||||
|
window.open(`intent://send?text=${encoded}#Intent;scheme=whatsapp;package=${pkg};end`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareViaWhatsAppMessenger(text: string) {
|
||||||
|
if (/android/i.test(navigator.userAgent)) {
|
||||||
|
openWAAndroid(text, 'com.whatsapp');
|
||||||
|
} else {
|
||||||
|
openWAWeb(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareViaWhatsAppBusiness(text: string) {
|
||||||
|
if (/android/i.test(navigator.userAgent)) {
|
||||||
|
openWAAndroid(text, 'com.whatsapp.w4b');
|
||||||
|
} else {
|
||||||
|
openWAWeb(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type CuttingCardRowParams = {
|
export type CuttingCardRowParams = {
|
||||||
cutting: Cutting;
|
cutting: Cutting;
|
||||||
index: number;
|
index: number;
|
||||||
@ -26,6 +103,8 @@ export function CuttingCardRow({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}: CuttingCardRowParams) {
|
}: CuttingCardRowParams) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
const items = cutting.cutting_materials ?? [];
|
const items = cutting.cutting_materials ?? [];
|
||||||
const result = cutting.cutting_results?.[0];
|
const result = cutting.cutting_results?.[0];
|
||||||
const singleCount = items.filter(
|
const singleCount = items.filter(
|
||||||
@ -43,6 +122,18 @@ export function CuttingCardRow({
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shareText = generateWhatsappText(cutting);
|
||||||
|
|
||||||
|
function handleShareWAMessenger() {
|
||||||
|
shareViaWhatsAppMessenger(shareText);
|
||||||
|
setShareOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShareWABusiness() {
|
||||||
|
shareViaWhatsAppBusiness(shareText);
|
||||||
|
setShareOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
@ -84,7 +175,7 @@ export function CuttingCardRow({
|
|||||||
|
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||||
{formatDateTime(cutting.created_at)}
|
{cutting.formatted_created_at}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||||
{cutting.status === 'completed'
|
{cutting.status === 'completed'
|
||||||
@ -110,20 +201,20 @@ export function CuttingCardRow({
|
|||||||
{formatNumber(result.cutting_result)}
|
{formatNumber(result.cutting_result)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{cutting.cost_per_unit && (
|
{cutting.formatted_cost_per_unit && (
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<span className="font-normal text-muted-foreground">
|
<span className="font-normal text-muted-foreground">
|
||||||
Per Produk:{' '}
|
Per Produk:{' '}
|
||||||
</span>
|
</span>
|
||||||
{formatCurrency(cutting.cost_per_unit)}
|
{cutting.formatted_cost_per_unit}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{cutting.total_material_cost && (
|
{cutting.formatted_total_material_cost && (
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<span className="font-normal text-muted-foreground">
|
<span className="font-normal text-muted-foreground">
|
||||||
Biaya Keseluruhan:{' '}
|
Biaya Keseluruhan:{' '}
|
||||||
</span>
|
</span>
|
||||||
{formatCurrency(cutting.total_material_cost)}
|
{cutting.formatted_total_material_cost}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -134,34 +225,66 @@ export function CuttingCardRow({
|
|||||||
srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
|
srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
|
||||||
modalSrc={cutting.photo_url}
|
modalSrc={cutting.photo_url}
|
||||||
title={productName}
|
title={productName}
|
||||||
description={cutting.description ?? formatDateTime(cutting.created_at)}
|
description={cutting.description ?? cutting.formatted_created_at}
|
||||||
className="h-16 w-16"
|
className="h-16 w-16"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(can('cuttings.update') || can('cuttings.delete')) && (
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<RowActions
|
<Tooltip>
|
||||||
actions={[
|
<Popover open={shareOpen} onOpenChange={setShareOpen}>
|
||||||
{
|
<TooltipTrigger asChild>
|
||||||
label: 'Edit',
|
<PopoverTrigger asChild>
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
show: can('cuttings.update'),
|
<Share2 className="h-4 w-4" />
|
||||||
onClick: () => onEdit(cutting),
|
</Button>
|
||||||
},
|
</PopoverTrigger>
|
||||||
{
|
</TooltipTrigger>
|
||||||
label: 'Hapus',
|
<TooltipContent>Bagikan</TooltipContent>
|
||||||
icon: (
|
<PopoverContent align="end" className="w-48 space-y-1 p-2">
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Button
|
||||||
),
|
variant="ghost"
|
||||||
show: can('cuttings.delete'),
|
size="sm"
|
||||||
onClick: () => onDelete(cutting),
|
className="w-full justify-start gap-2"
|
||||||
},
|
onClick={handleShareWAMessenger}
|
||||||
]}
|
>
|
||||||
wrapperClassName="flex shrink-0 items-center gap-1"
|
WhatsApp Messenger
|
||||||
/>
|
</Button>
|
||||||
)}
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={handleShareWABusiness}
|
||||||
|
>
|
||||||
|
WhatsApp Business
|
||||||
|
</Button>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</Tooltip>
|
||||||
|
{(can('cuttings.update') || can('cuttings.delete')) && (
|
||||||
|
<RowActions
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
|
show: can('cuttings.update'),
|
||||||
|
onClick: () => onEdit(cutting),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
icon: (
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
),
|
||||||
|
show: can('cuttings.delete'),
|
||||||
|
onClick: () => onDelete(cutting),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { Fragment } from 'react';
|
|
||||||
import { ImagePreviewButton } from '@/components/dialogs';
|
import { ImagePreviewButton } from '@/components/dialogs';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -9,6 +8,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
|
import { Fragment } from 'react';
|
||||||
import type { Cutting } from './columns';
|
import type { Cutting } from './columns';
|
||||||
|
|
||||||
export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
||||||
@ -23,8 +23,8 @@ export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
|||||||
const comboId = item.combination_id!;
|
const comboId = item.combination_id!;
|
||||||
|
|
||||||
if (!comboGroups[comboId]) {
|
if (!comboGroups[comboId]) {
|
||||||
comboGroups[comboId] = [];
|
comboGroups[comboId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
comboGroups[comboId].push(item);
|
comboGroups[comboId].push(item);
|
||||||
});
|
});
|
||||||
@ -35,8 +35,8 @@ comboGroups[comboId] = [];
|
|||||||
item.raw_material_price?.raw_material?.name ?? 'BING';
|
item.raw_material_price?.raw_material?.name ?? 'BING';
|
||||||
|
|
||||||
if (!acc[name]) {
|
if (!acc[name]) {
|
||||||
acc[name] = [];
|
acc[name] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
acc[name].push(item);
|
acc[name].push(item);
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ acc[name] = [];
|
|||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -113,67 +113,67 @@ acc[name] = [];
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{groupItems.map((item) => {
|
{groupItems.map((item) => {
|
||||||
counter++;
|
counter++;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{counter}
|
{counter}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{item.raw_material_price
|
{item.raw_material_price
|
||||||
?.photo_url ? (
|
?.photo_url ? (
|
||||||
<ImagePreviewButton
|
<ImagePreviewButton
|
||||||
srcs={[
|
srcs={[
|
||||||
item
|
|
||||||
.raw_material_price
|
|
||||||
.photo_conversion_url ??
|
|
||||||
item
|
item
|
||||||
.raw_material_price
|
.raw_material_price
|
||||||
.photo_url,
|
.photo_conversion_url ??
|
||||||
]}
|
item
|
||||||
modalSrc={
|
.raw_material_price
|
||||||
item
|
.photo_url,
|
||||||
.raw_material_price
|
]}
|
||||||
.photo_url
|
modalSrc={
|
||||||
}
|
item
|
||||||
title={
|
.raw_material_price
|
||||||
item
|
.photo_url
|
||||||
.raw_material_price
|
}
|
||||||
.variant
|
title={
|
||||||
}
|
item
|
||||||
/>
|
.raw_material_price
|
||||||
) : (
|
.variant
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
}
|
||||||
N/A
|
/>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
||||||
</TableCell>
|
N/A
|
||||||
<TableCell>
|
</div>
|
||||||
{item.raw_material_price
|
)}
|
||||||
?.variant ?? '-'}
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell className="text-right">
|
{item.raw_material_price
|
||||||
{formatNumber(
|
?.variant ?? '-'}
|
||||||
item.material_usage,
|
</TableCell>
|
||||||
)}
|
<TableCell className="text-right">
|
||||||
</TableCell>
|
{formatNumber(
|
||||||
<TableCell className="text-right">
|
item.material_usage,
|
||||||
{item.material_result !==
|
)}
|
||||||
null
|
</TableCell>
|
||||||
? formatNumber(
|
<TableCell className="text-right">
|
||||||
item.material_result,
|
{item.material_result !==
|
||||||
)
|
null
|
||||||
: '-'}
|
? formatNumber(
|
||||||
</TableCell>
|
item.material_result,
|
||||||
</TableRow>
|
)
|
||||||
);
|
: '-'}
|
||||||
})}
|
</TableCell>
|
||||||
</Fragment>
|
</TableRow>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
</Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ acc[name] = [];
|
|||||||
item
|
item
|
||||||
.raw_material_price
|
.raw_material_price
|
||||||
.photo_conversion_url ??
|
.photo_conversion_url ??
|
||||||
item
|
item
|
||||||
.raw_material_price
|
.raw_material_price
|
||||||
.photo_url,
|
.photo_url,
|
||||||
]}
|
]}
|
||||||
|
|||||||
183
resources/js/pages/admin/manage/cutting/show.tsx
Normal file
183
resources/js/pages/admin/manage/cutting/show.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { Head, Link } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
import { ImagePreviewButton } from '@/components/dialogs';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { formatNumber } from '@/lib/format';
|
||||||
|
import { index as cuttingIndex } from '@/routes/admin/manage/cuttings';
|
||||||
|
import type { Cutting } from './columns';
|
||||||
|
import { CuttingItemSubRow } from './cutting-sub-row';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
cutting: Cutting;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_BADGE_CLASSES: Record<string, string> = {
|
||||||
|
completed: 'bg-green-100 text-green-800 hover:bg-green-100',
|
||||||
|
cancelled: 'bg-red-100 text-red-800 hover:bg-red-100',
|
||||||
|
in_progress: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CuttingShow({ cutting }: Props) {
|
||||||
|
const items = cutting.cutting_materials ?? [];
|
||||||
|
const result = cutting.cutting_results?.[0];
|
||||||
|
const totalUsage = items.reduce(
|
||||||
|
(sum, item) => sum + Number(item.material_usage),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const statusBadgeClass =
|
||||||
|
STATUS_BADGE_CLASSES[cutting.status] ?? STATUS_BADGE_CLASSES.in_progress;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head title={`Cutting #${cutting.id}`} />
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">
|
||||||
|
Detail Cutting
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">
|
||||||
|
Rincian data proses cutting
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-lg border bg-card shadow-sm">
|
||||||
|
<div className="border-b bg-muted/30 p-6">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
Cutting #{cutting.id}
|
||||||
|
</h2>
|
||||||
|
<Badge variant="secondary" className={statusBadgeClass}>
|
||||||
|
{cutting.status === 'completed'
|
||||||
|
? 'Selesai'
|
||||||
|
: cutting.status === 'cancelled'
|
||||||
|
? 'Dibatalkan'
|
||||||
|
: 'Dikerjakan'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 space-y-1 text-sm text-muted-foreground">
|
||||||
|
<p>{cutting.formatted_created_at}</p>
|
||||||
|
<p>
|
||||||
|
Oleh{' '}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{cutting.created_by?.user_profile?.full_name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{cutting.description && (
|
||||||
|
<p className="mt-3 text-sm">{cutting.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cutting.photo_url && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<ImagePreviewButton
|
||||||
|
srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
|
||||||
|
modalSrc={cutting.photo_url}
|
||||||
|
title={result?.product_name ?? `Cutting #${cutting.id}`}
|
||||||
|
description={cutting.description ?? ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="border-b p-6">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">Hasil Produk</h3>
|
||||||
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Produk</TableHead>
|
||||||
|
<TableHead className="text-right">Hasil</TableHead>
|
||||||
|
<TableHead className="text-right">Sample</TableHead>
|
||||||
|
<TableHead className="text-right">Diluar Sample</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{result.product_name ?? '-'}</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{result.cutting_result ?? '-'} pcs
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{result.sample ?? '-'} pcs
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right tabular-nums">
|
||||||
|
{result.original_outside_sample ?? '-'} pcs
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="border-b p-6">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">Bahan Baku</h3>
|
||||||
|
|
||||||
|
<div className="pb-2">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Total Pemakaian</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{formatNumber(totalUsage)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{result?.cutting_result && (
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Total Hasil Cutting</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{formatNumber(result.cutting_result)} pcs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cutting.formatted_cost_per_unit && (
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Biaya Per Produk</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{cutting.formatted_cost_per_unit}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cutting.formatted_total_material_cost && (
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Total Biaya Bahan</p>
|
||||||
|
<p className="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{cutting.formatted_total_material_cost}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i className="text-xs md:hidden">Geser kesamping untuk melihat lebih banyak</i>
|
||||||
|
<CuttingItemSubRow cutting={cutting} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href={cuttingIndex.url()}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Kembali
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Data ini dibagikan dari sistem DST Collection
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -64,7 +64,7 @@
|
|||||||
Route::prefix('manage')->name('admin.manage.')->group(function () {
|
Route::prefix('manage')->name('admin.manage.')->group(function () {
|
||||||
Route::resource('purchases', PurchaseController::class)->except(['show'])->middleware('permission:purchases.view|purchases.create|purchases.update|purchases.delete');
|
Route::resource('purchases', PurchaseController::class)->except(['show'])->middleware('permission:purchases.view|purchases.create|purchases.update|purchases.delete');
|
||||||
|
|
||||||
Route::resource('cuttings', CuttingController::class)->except(['show'])->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::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