Merge branch 'feat/share-daily-checklist-to-wa' into 'development'

[FEAT/FE] Adjust Share Daily Checklist to Whatsapp

See merge request mbugroup/lti-web-client!429
This commit is contained in:
Rivaldi A N S
2026-04-25 05:34:44 +00:00
7 changed files with 355 additions and 102 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
#npm run format
#npm run lint
#npm run typecheck
#git add .
npm run format
npm run lint
npm run typecheck
git add .
@@ -1,6 +1,6 @@
'use client';
import { RefObject, useMemo } from 'react';
import { RefObject, useCallback, useMemo } from 'react';
import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
@@ -17,22 +17,31 @@ import {
import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { CustomerApi } from '@/services/api/master-data';
import { CustomerApi, ProductApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
import { ProjectFlockApi } from '@/services/api/production';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { Product } from '@/types/api/master-data/product';
interface MarketingFilterModal {
ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: MarketingFilter) => void;
onReset?: () => void;
initialValues?: {
product_ids: OptionType<number>[];
status: OptionType<string> | null;
customer: OptionType<number> | null;
project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null;
};
}
const MarketingFilterModal = ({
ref,
onSubmit,
onReset,
initialValues,
}: MarketingFilterModal) => {
const closeModalHandler = () => {
ref.current?.close();
@@ -40,36 +49,13 @@ const MarketingFilterModal = ({
// ===== OPTIONS =====
const {
rawData: productsRawData,
options: productsOptions,
isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue,
loadMore: loadMoreProducts,
} = useSelect<BaseMarketing>(
MarketingApi.basePath,
'id',
'so_number',
'search'
);
const productsOptions = useMemo(() => {
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
const productsMap = new Map<number, { value: number; label: string }>();
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
const product = so.product_warehouse?.product;
if (product?.id && product?.name) {
productsMap.set(product.id, {
value: product.id,
label: product.name,
});
}
});
});
return Array.from(productsMap.values());
}, [productsRawData]);
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
include_all: 'true',
});
const {
options: customersOptions,
@@ -102,7 +88,7 @@ const MarketingFilterModal = ({
];
const formik = useFormik<MarketingFilterFormValues>({
initialValues: {
initialValues: initialValues || {
product_ids: [],
status: null,
customer: null,
@@ -114,11 +100,17 @@ const MarketingFilterModal = ({
onSubmit: async (values) => {
const formattedValues: MarketingFilter = {
product_ids: values.product_ids.map((item) => Number(item.value)),
product_names: values.product_ids.map((item) => item.label),
status: values.status?.value.toString() || '',
status_name: values.status?.label || '-',
customer_id: Number(values.customer?.value),
project_flock_id: Number(values.project_flock?.value) || undefined,
customer_name: values.customer?.label || '-',
project_flock_id: values.project_flock?.value || undefined,
project_flock_name: values.project_flock?.label,
project_flock_kandang_id:
Number(values.project_flock_kandang?.value) || undefined,
project_flock_kandang_name:
values.project_flock_kandang?.label || undefined,
};
onSubmit?.(formattedValues);
@@ -131,6 +123,22 @@ const MarketingFilterModal = ({
},
});
const { resetForm } = formik;
const formikResetHandler = useCallback(() => {
resetForm({
values: {
product_ids: [],
status: null,
customer: null,
project_flock: null,
project_flock_kandang: null,
},
});
onReset?.();
closeModalHandler();
}, [resetForm, onReset, closeModalHandler]);
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_ids', val as OptionType[]);
};
@@ -176,7 +184,7 @@ const MarketingFilterModal = ({
>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
onReset={formikResetHandler}
className='w-full flex flex-col'
>
{/* Modal Header */}
@@ -189,10 +189,15 @@ const MarketingTable = () => {
initial: {
search: '',
product_ids: '',
product_names: '',
status: '',
status_name: '',
customer_id: '',
customer_name: '',
project_flock_id: '',
project_flock_name: '',
project_flock_kandang_id: '',
project_flock_kandang_name: '',
},
paramMap: {
page: 'page',
@@ -203,6 +208,13 @@ const MarketingTable = () => {
project_flock_id: 'project_flock_id',
project_flock_kandang_id: 'project_flock_kandang_id',
},
excludeKeysFromUrl: [
'product_names',
'status_name',
'customer_name',
'project_flock_name',
'project_flock_kandang_name',
],
persist: true,
storeName: 'marketing-table',
@@ -225,17 +237,21 @@ const MarketingTable = () => {
values.product_ids?.map((item) => item.toString()).join(','),
true
);
updateFilter('product_names', values.product_names?.join(','));
updateFilter('status', values.status ? values.status.toString() : '', true);
updateFilter('status_name', values.status_name, true);
updateFilter(
'customer_id',
values.customer_id ? values.customer_id.toString() : '',
true
);
updateFilter('customer_name', values.customer_name, true);
updateFilter(
'project_flock_id',
values.project_flock_id ? values.project_flock_id.toString() : '',
true
);
updateFilter('project_flock_name', values.project_flock_name ?? '', true);
updateFilter(
'project_flock_kandang_id',
values.project_flock_kandang_id
@@ -243,6 +259,11 @@ const MarketingTable = () => {
: '',
true
);
updateFilter(
'project_flock_kandang_name',
values.project_flock_kandang_name ?? '',
true
);
};
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
@@ -250,10 +271,15 @@ const MarketingTable = () => {
const filterResetHandler = () => {
updateFilter('product_ids', '', true);
updateFilter('product_names', '', true);
updateFilter('status', '', true);
updateFilter('status_name', '', true);
updateFilter('customer_id', '', true);
updateFilter('customer_name', '', true);
updateFilter('project_flock_id', '', true);
updateFilter('project_flock_name', '', true);
updateFilter('project_flock_kandang_id', '', true);
updateFilter('project_flock_kandang_name', '', true);
};
const approveClickHandler = () => {
@@ -333,6 +359,56 @@ const MarketingTable = () => {
? 'DELIVERY_ORDER'
: null;
const marketingFilterInitialValues = useMemo(() => {
const productIds = tableFilterState.product_ids
? tableFilterState.product_ids
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
const productLabels = tableFilterState.product_names
? tableFilterState.product_names
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
return {
product_ids: productIds.map((value, idx) => ({
value: Number(value),
label: productLabels[idx] || '-',
})),
status: tableFilterState.status
? {
value: tableFilterState.status,
label: tableFilterState.status_name,
}
: null,
customer: tableFilterState.customer_id
? {
value: Number(tableFilterState.customer_id),
label: tableFilterState.customer_name,
}
: null,
project_flock: tableFilterState.project_flock_id
? {
value: Number(tableFilterState.project_flock_id),
label: tableFilterState.project_flock_name,
}
: null,
project_flock_kandang: tableFilterState.project_flock_kandang_id
? {
value: Number(tableFilterState.project_flock_kandang_id),
label: tableFilterState.project_flock_kandang_name,
}
: null,
};
}, [tableFilterState]);
const approveMarketingHandler = async (notes: string) => {
if (idsToProcess.length === 0) {
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
@@ -774,7 +850,16 @@ const MarketingTable = () => {
<div className='flex flex-row gap-3'>
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search']}
excludeFields={[
'page',
'pageSize',
'search',
'product_names',
'status_name',
'customer_name',
'project_flock_name',
'project_flock_kandang_name',
]}
onClick={() => {
filterModal.openModal();
}}
@@ -1146,6 +1231,7 @@ const MarketingTable = () => {
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
initialValues={marketingFilterInitialValues}
/>
</>
);
@@ -643,6 +643,12 @@ const PurchaseTable = () => {
'search',
'filter_by',
'sort_by',
'product_category_name',
'supplier_name',
'area_name',
'location_name',
'project_flock_name',
'project_flock_kandang_name',
]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal}
@@ -53,7 +53,6 @@ import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories
const CATEGORIES = [
{ value: 'pullet_open', label: 'Pullet Open' },
{ value: 'pullet_close', label: 'Pullet Close' },
@@ -62,6 +61,14 @@ const CATEGORIES = [
{ value: 'empty_kandang', label: 'Kandang Kosong' },
];
const CATEGORY_LABELS: { [key: string]: string } = {
pullet_open: 'Pullet Open',
pullet_close: 'Pullet Close',
produksi_open: 'Produksi Open',
produksi_close: 'Produksi Close',
empty_kandang: 'Kandang Kosong',
};
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum',
@@ -246,7 +253,6 @@ export function DailyChecklistContent() {
}
}, [selectedCategory]);
// Format date for display
const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal';
const [year, month, day] = dateStr.split('-');
@@ -259,6 +265,36 @@ export function DailyChecklistContent() {
});
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
});
};
const isMobileDevice = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
const getStatusMessage = () => {
switch (checklistStatus) {
case 'DRAFT':
return 'Checklist harian perlu disubmit';
case 'SUBMITTED':
return 'Checklist harian menunggu persetujuan';
case 'APPROVED':
return 'Checklist harian telah disetujui';
case 'REJECTED':
return 'Checklist harian telah ditolak';
default:
return '';
}
};
// Fetch master data on mount
useEffect(() => {
setInitialLoading(false);
@@ -842,7 +878,43 @@ export function DailyChecklistContent() {
}
setChecklistStatus('SUBMITTED');
toast.success('Checklist berhasil disubmit untuk approval');
const shareToWhatsApp = () => {
const kandangName = kandangOptions.find(
(k) => String(k.value) === kandangId
)?.label || kandangId;
const statusMsg = getStatusMessage();
const category = selectedCategory || '';
const message = encodeURIComponent(
`Daily Checklist\n\nTanggal: ${formatDate(date)}\nKandang: ${kandangName}\nKategori: ${CATEGORY_LABELS[category] || category}\nStatus: SUBMITTED${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
);
const isMobile = isMobileDevice();
const whatsappUrl = isMobile
? `https://wa.me/?text=${message}`
: `https://web.whatsapp.com/send?text=${message}`;
window.open(whatsappUrl, '_blank');
};
toast.success('Checklist berhasil disubmit untuk approval', {
action: {
label: 'Bagikan ke WhatsApp',
onClick: shareToWhatsApp,
},
description: (
<button
onClick={() =>
router.push(
`/daily-checklist/list-daily-checklist/detail/?checklistId=${dailyChecklistId}`
)
}
className='text-blue-600 hover:text-blue-800 underline font-medium'
>
Lihat Detail
</button>
),
});
} catch (error) {
console.error('Error submitting:', error);
toast.error('Terjadi kesalahan');
@@ -556,27 +556,68 @@ export function DetailDailyChecklistContent() {
});
};
const shareHandler = async () => {
setIsGeneratingImage(true);
const htmlBlob = await htmlToImage.toBlob(document.body);
const imgFile = new File(
[htmlBlob!],
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
{
type: 'image/png',
}
const isMobileDevice = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
const getStatusMessage = () => {
switch (header?.status) {
case 'DRAFT':
return 'Checklist harian perlu disubmit';
case 'SUBMITTED':
return 'Checklist harian menunggu persetujuan';
case 'APPROVED':
return 'Checklist harian telah disetujui';
case 'REJECTED':
return 'Checklist harian telah ditolak';
default:
return '';
}
};
const shareHandler = async () => {
const isMobile = isMobileDevice();
if (isMobile) {
setIsGeneratingImage(true);
}
const baseTitle = `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`;
const statusMsg = getStatusMessage();
const statusInfo = `\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}`;
const urlMessage = `\n\nView full checklist: ${window.location.href}`;
const fullMessage = baseTitle + statusInfo + urlMessage;
let shareData: ShareData;
if (isMobile) {
const htmlBlob = await htmlToImage.toBlob(document.body);
const imgFile = new File(
[htmlBlob!],
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
{
type: 'image/png',
}
);
shareData = {
files: [imgFile],
title: baseTitle,
text: fullMessage,
url: window.location.href,
};
} else {
shareData = {
title: baseTitle,
text: fullMessage,
url: window.location.href,
};
}
setIsGeneratingImage(false);
const shareData = {
files: [imgFile],
title: `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`,
text: `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`,
url: window.location.href,
};
try {
if (!navigator.canShare(shareData)) {
toast.error(
@@ -592,6 +633,25 @@ export function DetailDailyChecklistContent() {
}
};
const shareToWhatsAppHandler = async () => {
const isMobile = isMobileDevice();
setIsGeneratingImage(true);
const statusMsg = getStatusMessage();
const category = header?.category || '';
const message = encodeURIComponent(
`Daily Checklist\n\nTanggal: ${formatDate(header?.date || '')}\nKandang: ${header?.kandang_name}\nKategori: ${CATEGORY_LABELS[category] || category}\nProgress: ${header?.progress_percent}%\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
);
setIsGeneratingImage(false);
const whatsappUrl = isMobile
? `https://wa.me/?text=${message}`
: `https://web.whatsapp.com/send?text=${message}`;
window.open(whatsappUrl, '_blank');
};
if (loading) {
return (
<div className='min-h-screen'>
@@ -618,8 +678,8 @@ export function DetailDailyChecklistContent() {
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title with Back Button */}
<div className='mb-6 flex items-center gap-4'>
{/* Action Buttons */}
<div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
<Button
variant='outline'
size='sm'
@@ -630,51 +690,67 @@ export function DetailDailyChecklistContent() {
Kembali
</Button>
<div className='flex-1'>
<h1 className='text-2xl font-semibold text-gray-900'>
Detail Daily Checklist
</h1>
<p className='text-sm text-gray-600 mt-1'>
Lihat detail checklist harian
</p>
<div className='flex items-center gap-2 flex-wrap'>
{header.status === 'SUBMITTED' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<div className='flex gap-2 flex-wrap'>
<Button
onClick={handleApprove}
disabled={actionLoading}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-2' />
Approve
</Button>
<Button
onClick={handleReject}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-2' />
Reject
</Button>
</div>
</RequirePermission>
)}
<Button
variant='outline'
size='sm'
onClick={shareHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Share2 className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan'}
{isGeneratingImage && 'Memuat...'}
</Button>
<Button
variant='outline'
size='sm'
onClick={shareToWhatsAppHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Icon icon='mdi:whatsapp' className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan via WhatsApp'}
{isGeneratingImage && 'Memuat...'}
</Button>
</div>
</div>
{header.status === 'SUBMITTED' && (
<RequirePermission permissions='lti.daily_checklist.create'>
<div className='flex gap-2'>
<Button
onClick={handleApprove}
disabled={actionLoading}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-2' />
Approve
</Button>
<Button
onClick={handleReject}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-2' />
Reject
</Button>
</div>
</RequirePermission>
)}
<Button
variant='outline'
size='sm'
onClick={shareHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Share2 className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan'}
{isGeneratingImage && 'Memuat...'}
</Button>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Detail Daily Checklist
</h1>
<p className='text-sm text-gray-600 mt-1'>
Lihat detail checklist harian
</p>
</div>
{/* Header Info Card */}
+5
View File
@@ -95,10 +95,15 @@ export type Marketing = BaseMetadata & BaseMarketing;
*/
export type MarketingFilter = {
product_ids: number[];
product_names: string[];
status: string;
status_name: string;
customer_id: number;
customer_name: string;
project_flock_id?: number;
project_flock_name?: string;
project_flock_kandang_id?: number;
project_flock_kandang_name?: string;
};
/**