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
|
||||
{
|
||||
return $this->handleAction(
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
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'])]
|
||||
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]
|
||||
protected function cancelled(Builder $query): void
|
||||
{
|
||||
|
||||
@ -29,9 +29,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->with([
|
||||
'createdBy:id',
|
||||
'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.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
||||
'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant',
|
||||
'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||
'cuttingMaterialCombinations:id,cutting_id,material_result',
|
||||
])
|
||||
@ -80,6 +80,89 @@ public function getProductNames(): Collection
|
||||
->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
|
||||
{
|
||||
$cutting->load([
|
||||
|
||||
@ -24,7 +24,7 @@ createInertiaApp({
|
||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||
layout: (name) => {
|
||||
switch (true) {
|
||||
case name === 'welcome':
|
||||
case name === 'admin/manage/cutting/show':
|
||||
return null;
|
||||
case name.startsWith('auth/'):
|
||||
return AuthLayout;
|
||||
|
||||
@ -26,10 +26,13 @@ export type Cutting = {
|
||||
status: string;
|
||||
description: string | null;
|
||||
total_material_cost: number | null;
|
||||
formatted_total_material_cost: string | null;
|
||||
cost_per_unit: number | null;
|
||||
formatted_cost_per_unit: string | null;
|
||||
photo_url: string | null;
|
||||
photo_conversion_url: string | null;
|
||||
created_at: string;
|
||||
formatted_created_at: string;
|
||||
created_by: {
|
||||
id: number;
|
||||
user_profile: {
|
||||
@ -46,8 +49,6 @@ export type Cutting = {
|
||||
raw_material_price: {
|
||||
id: number;
|
||||
variant: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
photo_url: string | null;
|
||||
photo_conversion_url: string | null;
|
||||
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 { ImagePreviewButton } from '@/components/dialogs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { formatDateTime, formatNumber } from '@/lib/format';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { formatNumber } from '@/lib/format';
|
||||
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';
|
||||
|
||||
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 = {
|
||||
cutting: Cutting;
|
||||
index: number;
|
||||
@ -26,6 +103,8 @@ export function CuttingCardRow({
|
||||
onDelete,
|
||||
}: CuttingCardRowParams) {
|
||||
const { can } = useCan();
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const items = cutting.cutting_materials ?? [];
|
||||
const result = cutting.cutting_results?.[0];
|
||||
const singleCount = items.filter(
|
||||
@ -43,6 +122,18 @@ export function CuttingCardRow({
|
||||
0,
|
||||
);
|
||||
|
||||
const shareText = generateWhatsappText(cutting);
|
||||
|
||||
function handleShareWAMessenger() {
|
||||
shareViaWhatsAppMessenger(shareText);
|
||||
setShareOpen(false);
|
||||
}
|
||||
|
||||
function handleShareWABusiness() {
|
||||
shareViaWhatsAppBusiness(shareText);
|
||||
setShareOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<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 className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||
{cutting.status === 'completed'
|
||||
@ -110,20 +201,20 @@ export function CuttingCardRow({
|
||||
{formatNumber(result.cutting_result)}
|
||||
</span>
|
||||
)}
|
||||
{cutting.cost_per_unit && (
|
||||
{cutting.formatted_cost_per_unit && (
|
||||
<span className="font-semibold">
|
||||
<span className="font-normal text-muted-foreground">
|
||||
Per Produk:{' '}
|
||||
</span>
|
||||
{formatCurrency(cutting.cost_per_unit)}
|
||||
{cutting.formatted_cost_per_unit}
|
||||
</span>
|
||||
)}
|
||||
{cutting.total_material_cost && (
|
||||
{cutting.formatted_total_material_cost && (
|
||||
<span className="font-semibold">
|
||||
<span className="font-normal text-muted-foreground">
|
||||
Biaya Keseluruhan:{' '}
|
||||
</span>
|
||||
{formatCurrency(cutting.total_material_cost)}
|
||||
{cutting.formatted_total_material_cost}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -134,34 +225,66 @@ export function CuttingCardRow({
|
||||
srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
|
||||
modalSrc={cutting.photo_url}
|
||||
title={productName}
|
||||
description={cutting.description ?? formatDateTime(cutting.created_at)}
|
||||
description={cutting.description ?? cutting.formatted_created_at}
|
||||
className="h-16 w-16"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(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 className="flex shrink-0 items-center gap-1">
|
||||
<Tooltip>
|
||||
<Popover open={shareOpen} onOpenChange={setShareOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Bagikan</TooltipContent>
|
||||
<PopoverContent align="end" className="w-48 space-y-1 p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={handleShareWAMessenger}
|
||||
>
|
||||
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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Fragment } from 'react';
|
||||
import { ImagePreviewButton } from '@/components/dialogs';
|
||||
import {
|
||||
Table,
|
||||
@ -9,6 +8,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { formatNumber } from '@/lib/format';
|
||||
import { Fragment } from 'react';
|
||||
import type { Cutting } from './columns';
|
||||
|
||||
export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
||||
@ -23,8 +23,8 @@ export function CuttingItemSubRow({ cutting }: { cutting: Cutting }) {
|
||||
const comboId = item.combination_id!;
|
||||
|
||||
if (!comboGroups[comboId]) {
|
||||
comboGroups[comboId] = [];
|
||||
}
|
||||
comboGroups[comboId] = [];
|
||||
}
|
||||
|
||||
comboGroups[comboId].push(item);
|
||||
});
|
||||
@ -35,8 +35,8 @@ comboGroups[comboId] = [];
|
||||
item.raw_material_price?.raw_material?.name ?? 'BING';
|
||||
|
||||
if (!acc[name]) {
|
||||
acc[name] = [];
|
||||
}
|
||||
acc[name] = [];
|
||||
}
|
||||
|
||||
acc[name].push(item);
|
||||
|
||||
@ -51,7 +51,7 @@ acc[name] = [];
|
||||
let counter = 0;
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@ -113,67 +113,67 @@ acc[name] = [];
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{groupItems.map((item) => {
|
||||
counter++;
|
||||
{groupItems.map((item) => {
|
||||
counter++;
|
||||
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">
|
||||
{counter}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.raw_material_price
|
||||
?.photo_url ? (
|
||||
<ImagePreviewButton
|
||||
srcs={[
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_conversion_url ??
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">
|
||||
{counter}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.raw_material_price
|
||||
?.photo_url ? (
|
||||
<ImagePreviewButton
|
||||
srcs={[
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_url,
|
||||
]}
|
||||
modalSrc={
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_url
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.raw_material_price
|
||||
?.variant ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(
|
||||
item.material_usage,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.material_result !==
|
||||
null
|
||||
? formatNumber(
|
||||
item.material_result,
|
||||
)
|
||||
: '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
)}
|
||||
.raw_material_price
|
||||
.photo_conversion_url ??
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_url,
|
||||
]}
|
||||
modalSrc={
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_url
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.raw_material_price
|
||||
?.variant ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(
|
||||
item.material_usage,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{item.material_result !==
|
||||
null
|
||||
? formatNumber(
|
||||
item.material_result,
|
||||
)
|
||||
: '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -232,7 +232,7 @@ acc[name] = [];
|
||||
item
|
||||
.raw_material_price
|
||||
.photo_conversion_url ??
|
||||
item
|
||||
item
|
||||
.raw_material_price
|
||||
.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::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::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user