feat: add share functionality for cutting details with new RowShareAction component and Share pagex
This commit is contained in:
parent
e32a0138dd
commit
175ec30a5c
@ -109,4 +109,11 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
|
|||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function share(Cutting $cutting): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/manage/cuttings/Share', [
|
||||||
|
'cutting' => $this->cuttingService->findForShare($cutting),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -211,6 +211,21 @@ public function findForEdit(Cutting $cutting): Cutting
|
|||||||
return $cutting;
|
return $cutting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findForShare(Cutting $cutting): Cutting
|
||||||
|
{
|
||||||
|
$cutting->load([
|
||||||
|
'createdBy.profile',
|
||||||
|
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
|
'results.productVariant.product:id,name',
|
||||||
|
'results.productVariant:id,product_id,name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->appendCostPreview($cutting);
|
||||||
|
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
|
||||||
|
|
||||||
|
return $cutting;
|
||||||
|
}
|
||||||
|
|
||||||
public function draftMaterialsForUser(User $user): array
|
public function draftMaterialsForUser(User $user): array
|
||||||
{
|
{
|
||||||
return $this->draftMaterialsQuery($user)
|
return $this->draftMaterialsQuery($user)
|
||||||
|
|||||||
93
resources/js/components/button/RowShareAction.vue
Normal file
93
resources/js/components/button/RowShareAction.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Copy, MessageCircle, Share2 } from '@lucide/vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { share } from '@/routes/admin/manage/cuttings';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
cutting: CuttingListItem;
|
||||||
|
tooltip?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const copied = ref(false);
|
||||||
|
|
||||||
|
const shareLink = computed(() => {
|
||||||
|
return window.location.origin + share.url(props.cutting.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const whatsappText = computed(() => {
|
||||||
|
const c = props.cutting;
|
||||||
|
let text = `*Cutting #${c.id}*\n`;
|
||||||
|
text += `Status: ${c.status_label}\n`;
|
||||||
|
text += `Tanggal: ${c.created_at_formatted}\n`;
|
||||||
|
text += `Oleh: ${c.created_by?.profile?.full_name ?? c.created_by?.username ?? '-'}\n`;
|
||||||
|
|
||||||
|
if (c.description) {
|
||||||
|
text += `Deskripsi: ${c.description}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
text += `\nTotal Hasil Cutting: ${c.total_result_pieces ?? 0} pcs\n`;
|
||||||
|
text += `\nLihat detail lengkap:\n${shareLink.value}`;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function copyLink() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLink.value);
|
||||||
|
copied.value = true;
|
||||||
|
toast.success('Link berhasil disalin');
|
||||||
|
setTimeout(() => {
|
||||||
|
copied.value = false;
|
||||||
|
}, 2000);
|
||||||
|
open.value = false;
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal menyalin link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareViaWhatsApp() {
|
||||||
|
const encodedText = encodeURIComponent(whatsappText.value);
|
||||||
|
const url = `https://wa.me/?text=${encodedText}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tooltip>
|
||||||
|
<Popover v-model:open="open">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
|
||||||
|
<Share2 class="size-4" />
|
||||||
|
<span class="sr-only">{{ tooltip || 'Bagikan' }}</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent>{{ tooltip || 'Bagikan' }}</TooltipContent>
|
||||||
|
|
||||||
|
<PopoverContent align="end" class="w-48 space-y-1 p-2">
|
||||||
|
<Button variant="ghost" size="sm" class="w-full justify-start gap-2" @click="copyLink">
|
||||||
|
<Copy class="size-4" />
|
||||||
|
{{ copied ? 'Tersalin!' : 'Salin Link' }}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" class="w-full justify-start gap-2" @click="shareViaWhatsApp">
|
||||||
|
<MessageCircle class="size-4" />
|
||||||
|
WhatsApp
|
||||||
|
</Button>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
@ -9,5 +9,6 @@ export { default as RowStatusAction } from './RowStatusAction.vue';
|
|||||||
export { default as RowPrintAction } from './RowPrintAction.vue';
|
export { default as RowPrintAction } from './RowPrintAction.vue';
|
||||||
export { default as RowDetailAction } from './RowDetailAction.vue';
|
export { default as RowDetailAction } from './RowDetailAction.vue';
|
||||||
export { default as RowTransferAction } from './RowTransferAction.vue';
|
export { default as RowTransferAction } from './RowTransferAction.vue';
|
||||||
|
export { default as RowShareAction } from './RowShareAction.vue';
|
||||||
export { default as CreateButton } from './CreateButton.vue';
|
export { default as CreateButton } from './CreateButton.vue';
|
||||||
export { default as BackButton } from './BackButton.vue';
|
export { default as BackButton } from './BackButton.vue';
|
||||||
|
|||||||
197
resources/js/pages/admin/manage/cuttings/Share.vue
Normal file
197
resources/js/pages/admin/manage/cuttings/Share.vue
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
|
||||||
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
cutting: CuttingListItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface GroupedMaterials {
|
||||||
|
key: number;
|
||||||
|
name: string;
|
||||||
|
unitLabel?: string;
|
||||||
|
items: typeof props.cutting.materials;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedResults {
|
||||||
|
key: number;
|
||||||
|
name: string;
|
||||||
|
items: typeof props.cutting.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedMaterials = computed<GroupedMaterials[]>(() => {
|
||||||
|
const groups: Record<number, GroupedMaterials> = {};
|
||||||
|
|
||||||
|
props.cutting.materials.forEach((item) => {
|
||||||
|
const key = item.raw_material_id ?? 0;
|
||||||
|
const name = item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||||
|
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = { key, name, unitLabel: item.raw_material_unit_label, items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[key].items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(groups);
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedResults = computed<GroupedResults[]>(() => {
|
||||||
|
const groups: Record<number, GroupedResults> = {};
|
||||||
|
|
||||||
|
props.cutting.results.forEach((item) => {
|
||||||
|
const product = item.product_variant?.product;
|
||||||
|
const key = product?.id ?? 0;
|
||||||
|
const name = product?.name ?? 'Produk Tidak Diketahui';
|
||||||
|
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = { key, name, items: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[key].items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(groups);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-background">
|
||||||
|
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight">Detail Cutting</h1>
|
||||||
|
<p class="mt-1 text-muted-foreground">Rincian data proses cutting</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Card -->
|
||||||
|
<div class="overflow-hidden rounded-lg border bg-card shadow-sm">
|
||||||
|
<!-- Cutting Info Header -->
|
||||||
|
<div class="border-b bg-muted/30 p-6">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<h2 class="text-xl font-semibold">Cutting #{{ cutting.id }}</h2>
|
||||||
|
<Badge :variant="cuttingStatusBadgeVariant(cutting.status)">
|
||||||
|
{{ cutting.status_label }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 space-y-1 text-sm text-muted-foreground">
|
||||||
|
<p>{{ cutting.created_at_formatted }}</p>
|
||||||
|
<p>Oleh {{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="cutting.description" class="mt-3 text-sm">
|
||||||
|
{{ cutting.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary -->
|
||||||
|
<div class="border-b p-6">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold">Ringkasan</h3>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="rounded-lg border p-3">
|
||||||
|
<p class="text-xs text-muted-foreground">Total Hasil Cutting</p>
|
||||||
|
<p class="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{{ cutting.total_result_pieces ?? 0 }} pcs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="cutting.total_material_usage_summary_formatted" class="rounded-lg border p-3">
|
||||||
|
<p class="text-xs text-muted-foreground">Total Pemakaian Bahan</p>
|
||||||
|
<p class="mt-1 text-lg font-semibold text-primary">
|
||||||
|
{{ cutting.total_material_usage_summary_formatted }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Materials Section -->
|
||||||
|
<div v-if="cutting.materials.length" class="border-b p-6">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold">Bahan Baku</h3>
|
||||||
|
<div class="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Varian</TableHead>
|
||||||
|
<TableHead class="text-right">Pemakaian</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<template v-for="group in groupedMaterials" :key="group.key">
|
||||||
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
|
<TableCell colspan="2" class="font-semibold">
|
||||||
|
{{ group.name }}
|
||||||
|
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
|
||||||
|
{{ group.unitLabel }}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow v-for="material in group.items" :key="material.id">
|
||||||
|
<TableCell class="pl-6">
|
||||||
|
{{ material.variant }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ material.material_usage_formatted }}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Section -->
|
||||||
|
<div v-if="cutting.results.length" class="p-6">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold">Hasil Produk</h3>
|
||||||
|
<div class="overflow-hidden rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Varian</TableHead>
|
||||||
|
<TableHead class="text-right">Hasil</TableHead>
|
||||||
|
<TableHead class="text-right">Sample</TableHead>
|
||||||
|
<TableHead class="text-right">Diluar Sample</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<template v-for="group in groupedResults" :key="group.key">
|
||||||
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
|
<TableCell colspan="4" class="font-semibold">
|
||||||
|
{{ group.name }}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow v-for="result in group.items" :key="result.id">
|
||||||
|
<TableCell class="pl-6">
|
||||||
|
{{ result.product_variant?.name }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ result.cutting_result }} pcs
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ result.sample }} pcs
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right tabular-nums">
|
||||||
|
{{ result.original_outside_sample }} pcs
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="mt-6 text-center text-xs text-muted-foreground">
|
||||||
|
<p>Data ini dibagikan dari sistem DST Collection</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import { router } from '@inertiajs/vue3';
|
import { router } from '@inertiajs/vue3';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction, RowShareAction, RowStatusAction } from '@/components/button';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -104,6 +104,8 @@ function transitionStatus() {
|
|||||||
:label="action.label" :destructive="action.destructive" @click="openStatusConfirm(action)" />
|
:label="action.label" :destructive="action.destructive" @click="openStatusConfirm(action)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<RowShareAction :cutting="cutting" tooltip="Bagikan Data" />
|
||||||
|
|
||||||
<RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
|
<RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
|
||||||
|
|
||||||
<template v-if="canDelete">
|
<template v-if="canDelete">
|
||||||
|
|||||||
@ -346,6 +346,9 @@
|
|||||||
->middleware('permission:'.Permission::CUTTINGS_COMPLETE->value.'|'.Permission::CUTTINGS_VERIFY->value.'|'.Permission::CUTTINGS_REJECT->value)
|
->middleware('permission:'.Permission::CUTTINGS_COMPLETE->value.'|'.Permission::CUTTINGS_VERIFY->value.'|'.Permission::CUTTINGS_REJECT->value)
|
||||||
->name('transition_status');
|
->name('transition_status');
|
||||||
|
|
||||||
|
Route::get('{cutting}/share', [CuttingController::class, 'share'])
|
||||||
|
->name('share');
|
||||||
|
|
||||||
Route::delete('{cutting}', [CuttingController::class, 'destroy'])
|
Route::delete('{cutting}', [CuttingController::class, 'destroy'])
|
||||||
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_DELETE->value)
|
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_DELETE->value)
|
||||||
->name('destroy');
|
->name('destroy');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user