From 9f934ce5c7c7156bc0e4197d1697f0f4023ce759 Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 8 Sep 2025 23:12:11 +0700 Subject: [PATCH 01/42] fix local storage --- src/contexts/authContext.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/contexts/authContext.tsx b/src/contexts/authContext.tsx index 6f70ea5..b53c56a 100644 --- a/src/contexts/authContext.tsx +++ b/src/contexts/authContext.tsx @@ -25,7 +25,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const savedToken = localStorage.getItem('authToken') const savedUser = localStorage.getItem('user') if (savedToken) setToken(savedToken) - if (savedUser) setCurrentUser(JSON.parse(savedUser)) + if (savedUser) { + try { + setCurrentUser(JSON.parse(savedUser)) + } catch (error) { + console.error('Failed to parse saved user data:', error) + // Clear invalid data + localStorage.removeItem('user') + } + } setIsInitialized(true) }, []) From 789dd823b628893a5423eceb3af0d679947a015b Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 8 Sep 2025 23:19:41 +0700 Subject: [PATCH 02/42] add indonesian lang --- .../layout/shared/LanguageDropdown.tsx | 8 +- src/configs/i18n.ts | 3 +- src/data/dictionaries/ar.json | 116 ------------------ src/data/dictionaries/fr.json | 116 ------------------ src/data/dictionaries/id.json | 116 ++++++++++++++++++ src/utils/getDictionary.ts | 3 +- 6 files changed, 121 insertions(+), 241 deletions(-) delete mode 100644 src/data/dictionaries/ar.json delete mode 100644 src/data/dictionaries/fr.json create mode 100644 src/data/dictionaries/id.json diff --git a/src/components/layout/shared/LanguageDropdown.tsx b/src/components/layout/shared/LanguageDropdown.tsx index ac7f343..9a01c13 100644 --- a/src/components/layout/shared/LanguageDropdown.tsx +++ b/src/components/layout/shared/LanguageDropdown.tsx @@ -43,12 +43,8 @@ const languageData: LanguageDataType[] = [ langName: 'English' }, { - langCode: 'fr', - langName: 'French' - }, - { - langCode: 'ar', - langName: 'Arabic' + langCode: 'id', + langName: 'Indonesian' } ] diff --git a/src/configs/i18n.ts b/src/configs/i18n.ts index 6dde940..7607f8a 100644 --- a/src/configs/i18n.ts +++ b/src/configs/i18n.ts @@ -1,9 +1,10 @@ export const i18n = { defaultLocale: 'en', - locales: ['en', 'fr', 'ar'], + locales: ['en', 'id'], langDirection: { en: 'ltr', fr: 'ltr', + id: 'ltr', ar: 'rtl' } } as const diff --git a/src/data/dictionaries/ar.json b/src/data/dictionaries/ar.json deleted file mode 100644 index 7e0b702..0000000 --- a/src/data/dictionaries/ar.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "navigation": { - "dashboards": "لوحات القيادة", - "analytics": "تحليلات", - "inventory": "تجزئة الكترونية", - "stock": "المخزون", - "academy": "أكاديمية", - "logistics": "اللوجستية", - "frontPages": "الصفحات الأولى", - "landing": "الهبوط", - "pricing": "التسعير", - "payment": "قسط", - "checkout": "الدفع", - "helpCenter": "مركز المساعدة", - "appsPages": "التطبيقات والصفحات", - "apps": "تطبيقات", - "dashboard": "لوحة القيادة", - "products": "منتجات", - "list": "قائمة", - "add": "يضيف", - "restock": "استرجاع", - "category": "فئة", - "overview": "نظرة عامة", - "profitloss": "الربح والخسارة", - "finance": "مالية", - "paymentMethods": "طرق الدفع", - "organization": "المنظمة", - "outlet": "مخزن", - "units": "وحدات", - "reports": "تقارير", - "ingredients": "مكونات", - "orders": "أوامر", - "details": "تفاصيل", - "customers": "العملاء", - "manageReviews": "إدارة المراجعات", - "referrals": "الإحالات", - "settings": "إعدادات", - "myCourses": "دوراتي", - "courseDetails": "تفاصيل الدورة", - "fleet": "أسطول", - "email": "البريد الإلكتروني", - "chat": "محادثة", - "calendar": "تقويم", - "kanban": "كانبان", - "invoice": "فاتورة", - "preview": "معاينة", - "edit": "يحرر", - "user": "المستعمل", - "view": "رأي", - "rolesPermissions": "الأدوار والأذونات", - "roles": "الأدوار", - "permissions": "أذونات", - "pages": "الصفحات", - "userProfile": "ملف تعريفي للمستخدم", - "accountSettings": "إعدادت الحساب", - "faq": "التعليمات", - "miscellaneous": "متفرقات", - "comingSoon": "قريبا", - "underMaintenance": "تحت الصيانة", - "pageNotFound404": "الصفحة غير موجودة - 404", - "notAuthorized401": "غير مصرح به - 401", - "authPages": "صفحات المصادقة", - "login": "تسجيل الدخول", - "loginV1": "تسجيل الدخول v1", - "loginV2": "تسجيل الدخول الإصدار 2", - "register": "يسجل", - "registerV1": "تسجيل الإصدار 1", - "registerV2": "تسجيل الإصدار 2", - "registerMultiSteps": "تسجيل متعدد الخطوات", - "verifyEmail": "التحقق من البريد الإلكتروني", - "verifyEmailV1": "التحقق من البريد الإلكتروني الإصدار 1", - "verifyEmailV2": "التحقق من البريد الإلكتروني الإصدار 2", - "forgotPassword": "هل نسيت كلمة السر", - "forgotPasswordV1": "نسيت كلمة المرور v1", - "forgotPasswordV2": "نسيت كلمة المرور v2", - "resetPassword": "إعادة تعيين كلمة المرور", - "resetPasswordV1": "إعادة تعيين كلمة المرور v1", - "resetPasswordV2": "إعادة تعيين كلمة المرور الإصدار 2", - "twoSteps": "خطوتين", - "twoStepsV1": "خطوتين v1", - "twoStepsV2": "خطوتان - الإصدار 2", - "wizardExamples": "أمثلة على المعالج", - "propertyListing": "قائمة الممتلكات", - "createDeal": "إنشاء صفقة", - "dialogExamples": "أمثلة الحوار", - "widgetExamples": "أمثلة القطعة", - "basic": "أساسي", - "advanced": "متقدم", - "statistics": "إحصائيات", - "actions": "أجراءات", - "formsAndTables": "النماذج والجداول", - "formLayouts": "تخطيطات النموذج", - "formValidation": "التحقق من صحة النموذج", - "formWizard": "معالج النماذج", - "reactTable": "جدول رد الفعل", - "formELements": "عناصر النماذج", - "muiTables": "جداول MUI", - "chartsMisc": "الرسوم البيانية ومتفرقات", - "charts": "الرسوم البيانية", - "recharts": "يعيد رسم الخرائط", - "apex": "ذروة", - "foundation": "مؤسسة", - "components": "عناصر", - "menuExamples": "أمثلة القائمة", - "raiseSupport": "رفع الدعم", - "documentation": "توثيق", - "others": "آحرون", - "itemWithBadge": "العنصر مع شارة", - "externalLink": "رابط خارجي", - "menuLevels": "مستويات القائمة", - "menuLevel2": "مستوى القائمة 2", - "menuLevel3": "مستوى القائمة 3", - "disabledMenu": "قائمة المعوقين", - "dailyReport": "تقرير يومي" - } -} diff --git a/src/data/dictionaries/fr.json b/src/data/dictionaries/fr.json deleted file mode 100644 index a0ab0a6..0000000 --- a/src/data/dictionaries/fr.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "navigation": { - "dashboards": "Tableaux de bord", - "analytics": "Analytique", - "inventory": "Inventaire", - "stock": "Stock", - "academy": "Académie", - "logistics": "Logistique", - "frontPages": "Premières pages", - "landing": "Atterrissage", - "pricing": "Tarifs", - "payment": "Paiement", - "checkout": "Vérifier", - "helpCenter": "Centre d'aide", - "appsPages": "Applications et pages", - "apps": "Apps", - "dashboard": "Tableau de bord", - "products": "Produits", - "list": "Liste", - "add": "Ajouter", - "restock": "Rapprocher", - "category": "Catégorie", - "overview": "Aperçu", - "profitloss": "Profit et perte", - "finance": "Finance", - "paymentMethods": "Méthodes de paiement", - "organization": "Organisation", - "outlet": "Point de vente", - "units": "Unites", - "reports": "Rapports", - "ingredients": "Ingrédients", - "orders": "Ordres", - "details": "Détails", - "customers": "Clientes", - "manageReviews": "Gérer les avis", - "referrals": "Références", - "settings": "Paramètres", - "myCourses": "Mes cours", - "courseDetails": "Détails du cours", - "fleet": "Flotte", - "email": "E-mail", - "chat": "chatte", - "calendar": "Calendrier", - "kanban": "Kanban", - "invoice": "Facture d'achat", - "preview": "Aperçu", - "edit": "Éditer", - "user": "Utilisateur", - "view": "Voir", - "rolesPermissions": "Rôles et autorisations", - "roles": "Les rôles", - "permissions": "Autorisations", - "pages": "Pages", - "userProfile": "Profil de l'utilisateur", - "accountSettings": "Paramètres du compte", - "faq": "FAQ", - "miscellaneous": "Divers", - "comingSoon": "À venir", - "underMaintenance": "En maintenance", - "pageNotFound404": "Page non trouvée - 404", - "notAuthorized401": "Non autorisé - 401", - "authPages": "Pages d'authentification", - "login": "Connexion", - "loginV1": "Connexion v1", - "loginV2": "Connexion v2", - "register": "S'inscrire", - "registerV1": "Enregistrer v1", - "registerV2": "Enregistrer v2", - "registerMultiSteps": "Enregistrer plusieurs étapes", - "verifyEmail": "Vérifier les courriels", - "verifyEmailV1": "Vérifier l'e-mail v1", - "verifyEmailV2": "Vérifier l'e-mail v2", - "forgotPassword": "Mot de passe oublié", - "forgotPasswordV1": "Mot de passe oublié v1", - "forgotPasswordV2": "Mot de passe oublié v2", - "resetPassword": "Réinitialiser le mot de passe", - "resetPasswordV1": "Réinitialiser le mot de passe v1", - "resetPasswordV2": "Réinitialiser le mot de passe v2", - "twoSteps": "Deux étapes", - "twoStepsV1": "Deux étapes v1", - "twoStepsV2": "Deux étapes v2", - "wizardExamples": "Exemples d'assistants", - "propertyListing": "Liste des biens", - "createDeal": "Créer un accord", - "dialogExamples": "Exemples de dialogue", - "widgetExamples": "Exemples de widgets", - "basic": "Basique", - "advanced": "Avancée", - "statistics": "Statistiques", - "actions": "Actions", - "formsAndTables": "Formulaires et tableaux", - "formLayouts": "Dispositions de formulaire", - "formValidation": "Validation du formulaire", - "formWizard": "Assistant de formulaire", - "reactTable": "Tableau de réaction", - "formELements": "Éléments de formulaire", - "muiTables": "Tableaux MUI", - "chartsMisc": "Graphiques & Divers", - "charts": "Graphiques", - "recharts": "Regraphiques", - "apex": "Sommet", - "foundation": "fondation", - "components": "Composants", - "menuExamples": "Exemples de menus", - "raiseSupport": "Augmenter le soutien", - "documentation": "Documentation", - "others": "Les autres", - "itemWithBadge": "Article avec badge", - "externalLink": "Lien Externe", - "menuLevels": "Niveaux de menus", - "menuLevel2": "Niveau menu 2", - "menuLevel3": "Niveau menu 3", - "disabledMenu": "Menu désactivé", - "dailyReport": "Rapport quotidien" - } -} diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json new file mode 100644 index 0000000..e2fb3ff --- /dev/null +++ b/src/data/dictionaries/id.json @@ -0,0 +1,116 @@ +{ + "navigation": { + "dashboards": "Dasbor", + "analytics": "Analitik", + "inventory": "Inventaris", + "stock": "Stok", + "academy": "Akademi", + "logistics": "Logistik", + "frontPages": "Halaman Depan", + "landing": "Beranda", + "pricing": "Harga", + "payment": "Pembayaran", + "checkout": "Checkout", + "helpCenter": "Pusat Bantuan", + "appsPages": "Aplikasi & Halaman", + "apps": "Aplikasi", + "dashboard": "Dasbor", + "products": "Produk", + "list": "Daftar", + "add": "Tambah", + "restock": "Isi Ulang Stok", + "category": "Kategori", + "overview": "Ringkasan", + "profitloss": "Laba Rugi", + "units": "Unit", + "reports": "Laporan", + "finance": "Keuangan", + "paymentMethods": "Metode Pembayaran", + "organization": "Organisasi", + "outlet": "Outlet", + "ingredients": "Bahan", + "orders": "Pesanan", + "details": "Detail", + "customers": "Pelanggan", + "manageReviews": "Kelola Ulasan", + "referrals": "Rujukan", + "settings": "Pengaturan", + "myCourses": "Kursus Saya", + "courseDetails": "Detail Kursus", + "fleet": "Armada", + "email": "Email", + "chat": "Chat", + "calendar": "Kalender", + "kanban": "Kanban", + "invoice": "Faktur", + "preview": "Pratinjau", + "edit": "Edit", + "user": "Pengguna", + "view": "Lihat", + "rolesPermissions": "Peran & Izin", + "roles": "Peran", + "permissions": "Izin", + "pages": "Halaman", + "userProfile": "Profil Pengguna", + "accountSettings": "Pengaturan Akun", + "faq": "FAQ", + "miscellaneous": "Lain-lain", + "comingSoon": "Segera Hadir", + "underMaintenance": "Dalam Pemeliharaan", + "pageNotFound404": "Halaman Tidak Ditemukan - 404", + "notAuthorized401": "Tidak Diizinkan - 401", + "authPages": "Halaman Autentikasi", + "login": "Masuk", + "loginV1": "Masuk v1", + "loginV2": "Masuk v2", + "register": "Daftar", + "registerV1": "Daftar v1", + "registerV2": "Daftar v2", + "registerMultiSteps": "Daftar Multi-Langkah", + "verifyEmail": "Verifikasi Email", + "verifyEmailV1": "Verifikasi Email v1", + "verifyEmailV2": "Verifikasi Email v2", + "forgotPassword": "Lupa Kata Sandi", + "forgotPasswordV1": "Lupa Kata Sandi v1", + "forgotPasswordV2": "Lupa Kata Sandi v2", + "resetPassword": "Reset Kata Sandi", + "resetPasswordV1": "Reset Kata Sandi v1", + "resetPasswordV2": "Reset Kata Sandi v2", + "twoSteps": "Dua Langkah", + "twoStepsV1": "Dua Langkah v1", + "twoStepsV2": "Dua Langkah v2", + "wizardExamples": "Contoh Wizard", + "propertyListing": "Daftar Properti", + "createDeal": "Buat Penawaran", + "dialogExamples": "Contoh Dialog", + "widgetExamples": "Contoh Widget", + "basic": "Dasar", + "advanced": "Lanjutan", + "statistics": "Statistik", + "actions": "Aksi", + "formsAndTables": "Form & Tabel", + "formLayouts": "Layout Form", + "formValidation": "Validasi Form", + "formWizard": "Wizard Form", + "reactTable": "Tabel React", + "formELements": "Elemen Form", + "muiTables": "Tabel MUI", + "chartsMisc": "Grafik & Lain-lain", + "charts": "Grafik", + "recharts": "Recharts", + "apex": "Apex", + "foundation": "Fondasi", + "components": "Komponen", + "menuExamples": "Contoh Menu", + "raiseSupport": "Buat Tiket Dukungan", + "documentation": "Dokumentasi", + "others": "Lainnya", + "itemWithBadge": "Item dengan Badge", + "externalLink": "Link Eksternal", + "menuLevels": "Level Menu", + "menuLevel2": "Level Menu 2", + "menuLevel3": "Level Menu 3", + "disabledMenu": "Menu Nonaktif", + "dailyReport": "Laporan Harian" + } +} diff --git a/src/utils/getDictionary.ts b/src/utils/getDictionary.ts index caa8acd..6111d81 100644 --- a/src/utils/getDictionary.ts +++ b/src/utils/getDictionary.ts @@ -3,8 +3,7 @@ import type { Locale } from '@configs/i18n' const dictionaries = { en: () => import('@/data/dictionaries/en.json').then(module => module.default), - fr: () => import('@/data/dictionaries/fr.json').then(module => module.default), - ar: () => import('@/data/dictionaries/ar.json').then(module => module.default) + id: () => import('@/data/dictionaries/id.json').then(module => module.default) } export const getDictionary = async (locale: Locale) => dictionaries[locale]() From 105d75c820f2137bb44c7e2a8d1dfc16c1e38055 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 01:17:01 +0700 Subject: [PATCH 03/42] feat: vendor create and vendor list --- .../(private)/apps/vendor/list/page.tsx | 8 + .../layout/vertical/VerticalMenu.tsx | 8 +- src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/vendor.ts | 204 ++++++ src/types/apps/vendorTypes.ts | 10 + .../apps/vendor/list/AddVendorDrawer.tsx | 595 ++++++++++++++++++ src/views/apps/vendor/list/TableFilters.tsx | 109 ++++ .../apps/vendor/list/VendorListCards.tsx | 80 +++ .../apps/vendor/list/VendorListTable.tsx | 365 +++++++++++ src/views/apps/vendor/list/index.tsx | 24 + 11 files changed, 1404 insertions(+), 5 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/vendor/list/page.tsx create mode 100644 src/data/dummy/vendor.ts create mode 100644 src/types/apps/vendorTypes.ts create mode 100644 src/views/apps/vendor/list/AddVendorDrawer.tsx create mode 100644 src/views/apps/vendor/list/TableFilters.tsx create mode 100644 src/views/apps/vendor/list/VendorListCards.tsx create mode 100644 src/views/apps/vendor/list/VendorListTable.tsx create mode 100644 src/views/apps/vendor/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/vendor/list/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/vendor/list/page.tsx new file mode 100644 index 0000000..d9efb5c --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/vendor/list/page.tsx @@ -0,0 +1,8 @@ +import { vendorDummyData } from '@/data/dummy/vendor' +import VendorList from '@/views/apps/vendor/list' + +const VendorListTablePage = async () => { + return +} + +export default VendorListTablePage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index e235783..648248d 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -120,9 +120,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].list} - - {dictionary['navigation'].restock} - + {dictionary['navigation'].restock} {/* {dictionary['navigation'].settings} */} @@ -140,6 +138,10 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].list} {/* {dictionary['navigation'].view} */} + }> + {dictionary['navigation'].list} + {/* {dictionary['navigation'].view} */} + diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 2b821c4..9904156 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -111,6 +111,7 @@ "menuLevel2": "Menu Level 2", "menuLevel3": "Menu Level 3", "disabledMenu": "Disabled Menu", - "dailyReport": "Daily Report" + "dailyReport": "Daily Report", + "vendor": "Vendor" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index e2fb3ff..af683c1 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -111,6 +111,7 @@ "menuLevel2": "Level Menu 2", "menuLevel3": "Level Menu 3", "disabledMenu": "Menu Nonaktif", - "dailyReport": "Laporan Harian" + "dailyReport": "Laporan Harian", + "vendor": "Vendor" } } diff --git a/src/data/dummy/vendor.ts b/src/data/dummy/vendor.ts new file mode 100644 index 0000000..d95ddcd --- /dev/null +++ b/src/data/dummy/vendor.ts @@ -0,0 +1,204 @@ +import { VendorType } from '@/types/apps/vendorTypes' + +export const vendorDummyData: VendorType[] = [ + { + id: 1, + photo: '', + name: 'Budi Santoso', + company: 'PT Maju Bersama Sejahtera', + email: 'budi.santoso@majubersama.co.id', + telephone: '+62 21 5551234', + youPayable: 25500000, + theyPayable: 12300000 + }, + { + id: 2, + photo: '', + name: 'Siti Nurhaliza', + company: 'CV Berkah Mandiri', + email: 'siti.nurhaliza@berkahmandiri.com', + telephone: '+62 22 8887654', + youPayable: 18750000, + theyPayable: 8950000 + }, + { + id: 3, + photo: '', + name: 'Ahmad Wijaya', + company: 'PT Teknologi Nusantara', + email: 'ahmad.wijaya@teknusantara.co.id', + telephone: '+62 24 3332211', + youPayable: 42100000, + theyPayable: 15600000 + }, + { + id: 4, + photo: '', + name: 'Dewi Sartika', + company: 'UD Sumber Rejeki', + email: 'dewi.sartika@sumberrejeki.net', + telephone: '+62 31 4445566', + youPayable: 9800000, + theyPayable: 22100000 + }, + { + id: 5, + photo: '', + name: 'Rudi Hermawan', + company: 'PT Indah Karya Persada', + email: 'rudi.hermawan@indahkarya.co.id', + telephone: '+62 274 7778899', + youPayable: 33250000, + theyPayable: 5400000 + }, + { + id: 6, + photo: '', + name: 'Maya Sari', + company: 'CV Harapan Jaya', + email: 'maya.sari@harapanjaya.com', + telephone: '+62 261 1112233', + youPayable: 16900000, + theyPayable: 28750000 + }, + { + id: 7, + photo: '', + name: 'Andi Prasetyo', + company: 'PT Cipta Mandiri Utama', + email: 'andi.prasetyo@ciptamandiri.co.id', + telephone: '+62 411 5556677', + youPayable: 21400000, + theyPayable: 11850000 + }, + { + id: 8, + photo: '', + name: 'Fitri Ramadhani', + company: 'UD Barokah Sukses', + email: 'fitri.ramadhani@barokahsukses.net', + telephone: '+62 751 9998877', + youPayable: 12650000, + theyPayable: 19300000 + }, + { + id: 9, + photo: '', + name: 'Agus Setiawan', + company: 'PT Nusantara Prima', + email: 'agus.setiawan@nusantaraprima.co.id', + telephone: '+62 541 3334455', + youPayable: 38800000, + theyPayable: 7200000 + }, + { + id: 10, + photo: '', + name: 'Rina Sulastri', + company: 'CV Mitra Sejati', + email: 'rina.sulastri@mitrasejati.com', + telephone: '+62 778 6667788', + youPayable: 14300000, + theyPayable: 24950000 + }, + { + id: 11, + photo: '', + name: 'Bambang Kurniawan', + company: 'PT Harmoni Bersama', + email: 'bambang.kurniawan@harmonibersama.co.id', + telephone: '+62 21 7779900', + youPayable: 29150000, + theyPayable: 13750000 + }, + { + id: 12, + photo: '', + name: 'Indah Permatasari', + company: 'UD Cahaya Abadi', + email: 'indah.permatasari@cahayaabadi.net', + telephone: '+62 361 2223344', + youPayable: 8900000, + theyPayable: 31200000 + }, + { + id: 13, + photo: '', + name: 'Dodi Supriadi', + company: 'PT Karya Gemilang', + email: 'dodi.supriadi@karyagemilang.co.id', + telephone: '+62 721 8889911', + youPayable: 36700000, + theyPayable: 9100000 + }, + { + id: 14, + photo: '', + name: 'Lestari Wulandari', + company: 'CV Anugrah Sentosa', + email: 'lestari.wulandari@anugrahsentosa.com', + telephone: '+62 741 4445566', + youPayable: 19850000, + theyPayable: 16400000 + }, + { + id: 15, + photo: '', + name: 'Hendra Gunawan', + company: 'PT Surya Mandala', + email: 'hendra.gunawan@suryamandala.co.id', + telephone: '+62 511 7778800', + youPayable: 27300000, + theyPayable: 21650000 + }, + { + id: 16, + photo: '', + name: 'Nurul Hidayah', + company: 'UD Rezeki Barokah', + email: 'nurul.hidayah@rezekibarokah.net', + telephone: '+62 431 1112200', + youPayable: 15200000, + theyPayable: 26800000 + }, + { + id: 17, + photo: '', + name: 'Teguh Prasetyo', + company: 'PT Dinamika Persada', + email: 'teguh.prasetyo@dinamikapersada.co.id', + telephone: '+62 62 5556600', + youPayable: 32900000, + theyPayable: 12150000 + }, + { + id: 18, + photo: '', + name: 'Sri Mulyani', + company: 'CV Berkah Mulia', + email: 'sri.mulyani@berkahmulia.com', + telephone: '+62 771 3337799', + youPayable: 11700000, + theyPayable: 29400000 + }, + { + id: 19, + photo: '', + name: 'Joko Widodo', + company: 'PT Makmur Sejahtera', + email: 'joko.widodo@makmursejahtera.co.id', + telephone: '+62 341 8882211', + youPayable: 24800000, + theyPayable: 18350000 + }, + { + id: 20, + photo: '', + name: 'Ratna Sari', + company: 'UD Sari Indah', + email: 'ratna.sari@sariindah.net', + telephone: '+62 717 9990011', + youPayable: 17950000, + theyPayable: 23600000 + } +] diff --git a/src/types/apps/vendorTypes.ts b/src/types/apps/vendorTypes.ts new file mode 100644 index 0000000..33de981 --- /dev/null +++ b/src/types/apps/vendorTypes.ts @@ -0,0 +1,10 @@ +export type VendorType = { + id: number + photo: string + name: string + company: string + email: string + telephone: string + youPayable: number + theyPayable: number +} diff --git a/src/views/apps/vendor/list/AddVendorDrawer.tsx b/src/views/apps/vendor/list/AddVendorDrawer.tsx new file mode 100644 index 0000000..5980159 --- /dev/null +++ b/src/views/apps/vendor/list/AddVendorDrawer.tsx @@ -0,0 +1,595 @@ +// React Imports +import { useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import Typography from '@mui/material/Typography' +import Divider from '@mui/material/Divider' +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' + +// Third-party Imports +import { useForm, Controller } from 'react-hook-form' + +// Types Imports +import type { VendorType } from '@/types/apps/vendorTypes' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' + +type Props = { + open: boolean + handleClose: () => void + vendorData?: VendorType[] + setData: (data: VendorType[]) => void +} + +type FormValidateType = { + name: string + company: string + email: string + telephone: string +} + +// Vars +const initialData = { + name: '', + company: '', + email: '', + telephone: '' +} + +const AddVendorDrawer = (props: Props) => { + // Props + const { open, handleClose, vendorData, setData } = props + + // States + const [showMore, setShowMore] = useState(false) + const [alamatPengiriman, setAlamatPengiriman] = useState(['']) + const [rekeningBank, setRekeningBank] = useState([ + { + bank: '', + cabang: '', + namaPemilik: '', + nomorRekening: '' + } + ]) + + // Hooks + const { + control, + reset: resetForm, + handleSubmit, + formState: { errors } + } = useForm({ + defaultValues: initialData + }) + + // Functions untuk alamat + const handleTambahAlamat = () => { + setAlamatPengiriman([...alamatPengiriman, '']) + } + + const handleHapusAlamat = (index: number) => { + if (alamatPengiriman.length > 1) { + const newAlamat = alamatPengiriman.filter((_, i) => i !== index) + setAlamatPengiriman(newAlamat) + } + } + + const handleChangeAlamat = (index: number, value: string) => { + const newAlamat = [...alamatPengiriman] + newAlamat[index] = value + setAlamatPengiriman(newAlamat) + } + + // Functions untuk rekening bank + const handleTambahRekening = () => { + setRekeningBank([ + ...rekeningBank, + { + bank: '', + cabang: '', + namaPemilik: '', + nomorRekening: '' + } + ]) + } + + const handleHapusRekening = (index: number) => { + if (rekeningBank.length > 1) { + const newRekening = rekeningBank.filter((_, i) => i !== index) + setRekeningBank(newRekening) + } + } + + const handleChangeRekening = (index: number, field: string, value: string) => { + const newRekening = [...rekeningBank] + newRekening[index] = { ...newRekening[index], [field]: value } + setRekeningBank(newRekening) + } + + const onSubmit = (data: FormValidateType) => { + const newVendor: VendorType = { + id: (vendorData?.length && vendorData?.length + 1) || 1, + photo: '', + name: data.name, + company: data.company, + email: data.email, + telephone: data.telephone, + youPayable: 0, + theyPayable: 0 + } + + setData([...(vendorData ?? []), newVendor]) + handleClose() + resetForm(initialData) + setAlamatPengiriman(['']) + setRekeningBank([ + { + bank: '', + cabang: '', + namaPemilik: '', + nomorRekening: '' + } + ]) + setShowMore(false) + } + + const handleReset = () => { + handleClose() + resetForm(initialData) + setAlamatPengiriman(['']) + setRekeningBank([ + { + bank: '', + cabang: '', + namaPemilik: '', + nomorRekening: '' + } + ]) + setShowMore(false) + } + + return ( + + {/* Sticky Header */} + +
+ Tambah Vendor Baru + + + +
+
+ + {/* Scrollable Content */} + +
onSubmit(data))}> +
+ {/* Tampilkan Foto */} +
+ + + Tampilkan Foto + +
+ + {/* Nama */} +
+ + Nama * + + + + + Tuan + Nyonya + Nona + Bapak + Ibu + + + + ( + + )} + /> + + +
+ + {/* Perusahaan dan Telepon */} + + + ( + + )} + /> + + + ( + + )} + /> + + + + {/* Email */} + ( + + )} + /> + + {/* Tampilkan selengkapnya */} + {!showMore && ( +
setShowMore(true)}> + + + Tampilkan selengkapnya + +
+ )} + + {/* Konten tambahan yang muncul saat showMore true */} + {showMore && ( + <> + {/* Alamat Penagihan */} +
+ + Alamat Penagihan + + +
+ + {/* Negara */} +
+ + Negara + + + Indonesia + +
+ + {/* Provinsi dan Kota */} + + + + Provinsi + + + Pilih Provinsi + DKI Jakarta + Jawa Barat + Jawa Tengah + Jawa Timur + + + + + Kota + + + Pilih Kota + Jakarta + Bandung + Surabaya + + + + + {/* Kecamatan dan Kelurahan */} + + + + Kecamatan + + + Pilih Kecamatan + + + + + Kelurahan + + + Pilih Kelurahan + + + + + {/* Tipe Kartu Identitas dan ID */} + + + + Tipe Kartu Identitas + + + Pilih Tipe Kartu Identitas + KTP + SIM + Paspor + + + + + ID Kartu Identitas + + + + + + {/* NPWP */} +
+ + NPWP + + +
+ + {/* Alamat Pengiriman */} +
+ + Alamat Pengiriman + + {alamatPengiriman.map((alamat, index) => ( +
+ handleChangeAlamat(index, e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderColor: index === 1 ? 'primary.main' : 'default' + } + }} + /> + {alamatPengiriman.length > 1 && ( + handleHapusAlamat(index)} + sx={{ + color: 'error.main', + border: 1, + borderColor: 'error.main', + '&:hover': { + backgroundColor: 'error.light', + borderColor: 'error.main' + } + }} + > + + + )} +
+ ))} +
+ + {/* Tambah Alamat Pengiriman */} +
+ + + Tambah Alamat Pengiriman + +
+ + {/* Rekening Bank */} +
+ + Rekening Bank + + {rekeningBank.map((rekening, index) => ( +
+
+
+ {/* Baris pertama: Bank & Cabang */} + + + handleChangeRekening(index, 'bank', e.target.value)} + > + Pilih Bank + BCA + Mandiri + BNI + BRI + + + + handleChangeRekening(index, 'cabang', e.target.value)} + /> + + + + {/* Baris kedua: Nama Pemilik & Nomor Rekening */} + + + handleChangeRekening(index, 'namaPemilik', e.target.value)} + /> + + + handleChangeRekening(index, 'nomorRekening', e.target.value)} + /> + + +
+ + {/* Tombol hapus di samping, sejajar dengan tengah kedua baris */} + {rekeningBank.length > 1 && ( +
+ handleHapusRekening(index)} + sx={{ + color: 'error.main', + border: 1, + borderColor: 'error.main', + '&:hover': { + backgroundColor: 'error.light', + borderColor: 'error.main' + } + }} + > + + +
+ )} +
+
+ ))} + +
+ + + Tambah Rekening Bank + +
+ + + + + Nomor + + + + + + Tanggal Lahir + + + + + +
+ + Deskripsi + + +
+ + {/* Button Sembunyikan di dalam konten */} +
setShowMore(false)}> + + + Sembunyikan + +
+
+ + )} +
+ +
+ + {/* Sticky Footer */} + +
+ + +
+
+
+ ) +} + +export default AddVendorDrawer diff --git a/src/views/apps/vendor/list/TableFilters.tsx b/src/views/apps/vendor/list/TableFilters.tsx new file mode 100644 index 0000000..c5e4da6 --- /dev/null +++ b/src/views/apps/vendor/list/TableFilters.tsx @@ -0,0 +1,109 @@ +// React Imports +import { useState, useEffect } from 'react' + +// MUI Imports +import CardContent from '@mui/material/CardContent' +import Grid from '@mui/material/Grid2' +import MenuItem from '@mui/material/MenuItem' + +// Type Imports +import type { VendorType } from '@/types/apps/vendorTypes' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' + +const TableFilters = ({ setData, tableData }: { setData: (data: VendorType[]) => void; tableData?: VendorType[] }) => { + // States + const [company, setCompany] = useState('') + const [payableRange, setPayableRange] = useState('') + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => { + const filteredData = tableData?.filter(vendor => { + // Filter by company type + if (company && !vendor.company.toLowerCase().includes(company.toLowerCase())) return false + + // Filter by payable range (You Payable) + if (payableRange) { + const payable = vendor.youPayable + switch (payableRange) { + case 'low': + if (payable >= 20000000) return false + break + case 'medium': + if (payable < 20000000 || payable >= 35000000) return false + break + case 'high': + if (payable < 35000000) return false + break + } + } + + // Filter by search term (name, company, email) + if (searchTerm) { + const search = searchTerm.toLowerCase() + const matchesName = vendor.name.toLowerCase().includes(search) + const matchesCompany = vendor.company.toLowerCase().includes(search) + const matchesEmail = vendor.email.toLowerCase().includes(search) + + if (!matchesName && !matchesCompany && !matchesEmail) return false + } + + return true + }) + + setData(filteredData || []) + }, [company, payableRange, searchTerm, tableData, setData]) + + return ( + + + + setCompany(e.target.value)} + slotProps={{ + select: { displayEmpty: true } + }} + > + Pilih Jenis Perusahaan + PT (Perseroan Terbatas) + CV (Commanditaire Vennootschap) + UD (Usaha Dagang) + + + + setPayableRange(e.target.value)} + slotProps={{ + select: { displayEmpty: true } + }} + > + Pilih Rentang Hutang + Rendah (< 20M) + Sedang (20M - 35M) + Tinggi (> 35M) + + + + setSearchTerm(e.target.value)} + /> + + + + ) +} + +export default TableFilters diff --git a/src/views/apps/vendor/list/VendorListCards.tsx b/src/views/apps/vendor/list/VendorListCards.tsx new file mode 100644 index 0000000..d88ffe8 --- /dev/null +++ b/src/views/apps/vendor/list/VendorListCards.tsx @@ -0,0 +1,80 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'Hutang Anda', + stats: 'Rp 542.340.000', + avatarIcon: 'tabler-credit-card', + avatarColor: 'error', + trend: 'positive', + trendNumber: '12%', + subtitle: 'Total yang harus dibayar' + }, + { + title: 'Piutang Anda', + stats: 'Rp 387.250.000', + avatarIcon: 'tabler-wallet', + avatarColor: 'success', + trend: 'positive', + trendNumber: '8%', + subtitle: 'Total yang harus dibayar vendor' + }, + { + title: 'Pembayaran Diterima', + stats: 'Rp 156.800.000', + avatarIcon: 'tabler-arrow-down-circle', + avatarColor: 'success', + trend: 'positive', + trendNumber: '23%', + subtitle: 'Bulan ini' + }, + { + title: 'Pembayaran Dikirim', + stats: 'Rp 89.450.000', + avatarIcon: 'tabler-arrow-up-circle', + avatarColor: 'warning', + trend: 'negative', + trendNumber: '5%', + subtitle: 'Bulan ini' + }, + { + title: 'Hutang Jatuh Tempo', + stats: 'Rp 67.890.000', + avatarIcon: 'tabler-clock-exclamation', + avatarColor: 'error', + trend: 'negative', + trendNumber: '15%', + subtitle: 'Pembayaran terlambat' + }, + { + title: 'Piutang Tertunda', + stats: 'Rp 134.560.000', + avatarIcon: 'tabler-clock-dollar', + avatarColor: 'info', + trend: 'positive', + trendNumber: '28%', + subtitle: 'Menunggu dari vendor' + } +] + +const VendorListCards = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default VendorListCards diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx new file mode 100644 index 0000000..284b160 --- /dev/null +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -0,0 +1,365 @@ +'use client' + +// React Imports +import { useEffect, useState, useMemo } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' + +// Third-party Imports +import classnames from 'classnames' +import { rankItem } from '@tanstack/match-sorter-utils' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFacetedMinMaxValues, + getPaginationRowModel, + getSortedRowModel +} from '@tanstack/react-table' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// Type Imports +import type { ThemeColor } from '@core/types' +import type { VendorType } from '@/types/apps/vendorTypes' +import type { Locale } from '@configs/i18n' + +// Component Imports +import TableFilters from './TableFilters' +import AddVendorDrawer from './AddVendorDrawer' +import OptionMenu from '@core/components/option-menu' +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' +import CustomAvatar from '@core/components/mui/Avatar' + +// Util Imports +import { getInitials } from '@/utils/getInitials' +import { getLocalizedUrl } from '@/utils/i18n' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { formatCurrency } from '@/utils/transform' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type VendorTypeWithAction = VendorType & { + action?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => { + // States + const [addVendorOpen, setAddVendorOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [data, setData] = useState(...[tableData]) + const [filteredData, setFilteredData] = useState(data) + const [globalFilter, setGlobalFilter] = useState('') + + // Hooks + const { lang: locale } = useParams() + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('name', { + header: 'Vendor', + cell: ({ row }) => ( +
+ {getAvatar({ photo: row.original.photo, name: row.original.name })} +
+ + + {row.original.name} + + + {row.original.email} +
+
+ ) + }), + columnHelper.accessor('company', { + header: 'Perusahaan', + cell: ({ row }) => ( +
+ + {row.original.company} +
+ ) + }), + columnHelper.accessor('telephone', { + header: 'Telepon', + cell: ({ row }) => {row.original.telephone} + }), + columnHelper.accessor('youPayable', { + header: () =>
Anda Hutang
, + cell: ({ row }) => ( +
+ + {formatCurrency(row.original.youPayable)} + +
+ ) + }), + columnHelper.accessor('theyPayable', { + header: () =>
Mereka Hutang
, + cell: ({ row }) => ( +
+ + {formatCurrency(row.original.theyPayable)} + +
+ ) + }) + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, filteredData] + ) + + const table = useReactTable({ + data: filteredData as VendorType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + globalFilter + }, + initialState: { + pagination: { + pageSize: 10 + } + }, + enableRowSelection: true, + globalFilterFn: fuzzyFilter, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + onGlobalFilterChange: setGlobalFilter, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getFacetedMinMaxValues: getFacetedMinMaxValues() + }) + + const getAvatar = (params: Pick) => { + const { photo, name } = params + + if (photo) { + return + } else { + return {getInitials(name as string)} + } + } + + return ( + <> + + + +
+ table.setPageSize(Number(e.target.value))} + className='max-sm:is-full sm:is-[70px]' + > + 10 + 25 + 50 + +
+ setGlobalFilter(String(value))} + placeholder='Cari Vendor' + className='max-sm:is-full' + /> + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ { + table.setPageIndex(page) + }} + /> +
+ setAddVendorOpen(!addVendorOpen)} + vendorData={data} + setData={setData} + /> + + ) +} + +export default VendorListTable diff --git a/src/views/apps/vendor/list/index.tsx b/src/views/apps/vendor/list/index.tsx new file mode 100644 index 0000000..482bbf7 --- /dev/null +++ b/src/views/apps/vendor/list/index.tsx @@ -0,0 +1,24 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { VendorType } from '@/types/apps/vendorTypes' + +// Component Imports +import VendorListTable from './VendorListTable' +import VendorListCards from './VendorListCards' + +const VendorList = ({ vendorData }: { vendorData?: VendorType[] }) => { + return ( + + + + + + + + + ) +} + +export default VendorList From 59f2d458548e8768443aeb73aefd16461db895a3 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 01:25:50 +0700 Subject: [PATCH 04/42] update add vendor --- .../apps/vendor/list/AddVendorDrawer.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/views/apps/vendor/list/AddVendorDrawer.tsx b/src/views/apps/vendor/list/AddVendorDrawer.tsx index 5980159..42a1c3d 100644 --- a/src/views/apps/vendor/list/AddVendorDrawer.tsx +++ b/src/views/apps/vendor/list/AddVendorDrawer.tsx @@ -57,6 +57,7 @@ const AddVendorDrawer = (props: Props) => { nomorRekening: '' } ]) + const [showPemetaanAkun, setShowPemetaanAkun] = useState(false) // Hooks const { @@ -137,6 +138,7 @@ const AddVendorDrawer = (props: Props) => { } ]) setShowMore(false) + setShowPemetaanAkun(false) } const handleReset = () => { @@ -152,6 +154,7 @@ const AddVendorDrawer = (props: Props) => { } ]) setShowMore(false) + setShowPemetaanAkun(false) } return ( @@ -531,6 +534,102 @@ const AddVendorDrawer = (props: Props) => { +
setShowPemetaanAkun(!showPemetaanAkun)}> + + + {showPemetaanAkun ? 'Sembunyikan pemetaan akun' : 'Tampilkan pemetaan akun'} + +
+ + {/* Konten Pemetaan Akun */} + {showPemetaanAkun && ( +
+ {/* Akun Hutang */} + + + + Akun Hutang + + + 2-20100 Hutang Usaha + 2-20200 Hutang Bank + 2-20300 Hutang Lainnya + + + + + Maksimal Hutang + + + + + + + {/* Akun Piutang */} + + + + Akun Piutang + + + 1-10100 Piutang Usaha + 1-10200 Piutang Karyawan + 1-10300 Piutang Lainnya + + + + + Maksimal Piutang + + + + + + + {/* Kena Pajak */} +
+ + Kena pajak ? + +
+ + +
+
+
+ )} + From dfa6b4be750735941b158a757002361ae6e3baf8 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 14:28:35 +0700 Subject: [PATCH 05/42] Vendor Detail --- .../(private)/apps/vendor/detail/page.tsx | 7 + src/data/dummy/vendor.ts | 282 +++++++++++++- src/types/apps/vendorTypes.ts | 13 + src/views/apps/vendor/detail/index.tsx | 20 + .../vendor-content/VendorMoneyInOutChart.tsx | 157 ++++++++ .../vendor-content/VendorSalesChart.tsx | 144 +++++++ .../detail/vendor-content/VendorStatistic.tsx | 63 ++++ .../vendor/detail/vendor-content/index.tsx | 21 ++ .../vendor/detail/vendor-content/styles.css | 7 + .../VendorDebsPayedTable.tsx | 334 ++++++++++++++++ .../vendor-debs-payed-table/index.tsx | 21 ++ .../VendorReceivablesPaymentTable.tsx | 327 ++++++++++++++++ .../index.tsx | 18 + .../VendorTransactionTable.tsx | 355 ++++++++++++++++++ .../vendor-transaction-table/index.tsx | 18 + .../detail/vendor-overview/VendorDetails.tsx | 134 +++++++ .../vendor/detail/vendor-overview/index.tsx | 17 + .../apps/vendor/list/VendorListTable.tsx | 2 +- 18 files changed, 1938 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx create mode 100644 src/views/apps/vendor/detail/index.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/VendorMoneyInOutChart.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/VendorSalesChart.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/VendorStatistic.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/index.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/styles.css create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/VendorDebsPayedTable.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/index.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/VendorReceivablesPaymentTable.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/index.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/VendorTransactionTable.tsx create mode 100644 src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/index.tsx create mode 100644 src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx create mode 100644 src/views/apps/vendor/detail/vendor-overview/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx new file mode 100644 index 0000000..5069723 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/vendor/detail/page.tsx @@ -0,0 +1,7 @@ +import VendorView from '@/views/apps/vendor/detail' + +const VendorViewPage = async () => { + return +} + +export default VendorViewPage diff --git a/src/data/dummy/vendor.ts b/src/data/dummy/vendor.ts index d95ddcd..a984a99 100644 --- a/src/data/dummy/vendor.ts +++ b/src/data/dummy/vendor.ts @@ -1,4 +1,4 @@ -import { VendorType } from '@/types/apps/vendorTypes' +import { VendorDebsPayedType, VendorTransactionType, VendorType } from '@/types/apps/vendorTypes' export const vendorDummyData: VendorType[] = [ { @@ -202,3 +202,283 @@ export const vendorDummyData: VendorType[] = [ theyPayable: 23600000 } ] + +// Dummy data untuk VendorDebsPayedType +export const vendorDebsPayedData: VendorDebsPayedType[] = [ + { + date: '2024-08-15', + transaction: 'Hutang ke PT Supplier Bahan Kimia', + reference: 'DEBT-001-2024', + total: 4500000 + }, + { + date: '2024-08-22', + transaction: 'Invoice Belum Dibayar #INV-2024-789', + reference: 'DEBT-002-2024', + total: 2750000 + }, + { + date: '2024-08-30', + transaction: 'Tagihan CV Mitra Sejahtera', + reference: 'DEBT-003-2024', + total: 1850000 + }, + { + date: '2024-09-05', + transaction: 'Hutang Pembelian Peralatan Kantor', + reference: 'DEBT-004-2024', + total: 3200000 + }, + { + date: '2024-09-08', + transaction: 'Tagihan Listrik & Utilities Bulan Lalu', + reference: 'DEBT-005-2024', + total: 950000 + }, + { + date: '2024-09-10', + transaction: 'Hutang ke PT Konstruksi Prima', + reference: 'DEBT-006-2024', + total: 12500000 + }, + { + date: '2024-09-12', + transaction: 'Invoice Jasa Konsultasi IT', + reference: 'DEBT-007-2024', + total: 6800000 + }, + { + date: '2024-09-15', + transaction: 'Tagihan Bahan Baku Produksi', + reference: 'DEBT-008-2024', + total: 8900000 + }, + { + date: '2024-09-18', + transaction: 'Hutang ke Supplier Packaging', + reference: 'DEBT-009-2024', + total: 2100000 + }, + { + date: '2024-09-20', + transaction: 'Invoice Maintenance Equipment', + reference: 'DEBT-010-2024', + total: 4300000 + }, + { + date: '2024-09-22', + transaction: 'Tagihan Jasa Logistik & Pengiriman', + reference: 'DEBT-011-2024', + total: 1650000 + }, + { + date: '2024-09-25', + transaction: 'Hutang Pembelian Software License', + reference: 'DEBT-012-2024', + total: 5400000 + }, + { + date: '2024-09-28', + transaction: 'Invoice Jasa Cleaning Service', + reference: 'DEBT-013-2024', + total: 750000 + }, + { + date: '2024-09-30', + transaction: 'Tagihan Sewa Gedung Bulan September', + reference: 'DEBT-014-2024', + total: 15000000 + }, + { + date: '2024-10-01', + transaction: 'Hutang ke CV Digital Marketing', + reference: 'DEBT-015-2024', + total: 3800000 + } +] + +export const vendorReceivablesData: VendorDebsPayedType[] = [ + { + date: '2024-08-20', + transaction: 'Piutang dari PT Mitra Sejahtera Abadi', + reference: 'RCV-001-2024', + total: 5500000 + }, + { + date: '2024-08-25', + transaction: 'Invoice Jual Produk #INV-2024-456', + reference: 'RCV-002-2024', + total: 3250000 + }, + { + date: '2024-09-02', + transaction: 'Tagihan ke CV Berkah Mandiri', + reference: 'RCV-003-2024', + total: 2800000 + }, + { + date: '2024-09-05', + transaction: 'Piutang Penjualan Barang Jadi', + reference: 'RCV-004-2024', + total: 4200000 + }, + { + date: '2024-09-08', + transaction: 'Invoice Jasa Konsultasi', + reference: 'RCV-005-2024', + total: 1750000 + }, + { + date: '2024-09-10', + transaction: 'Tagihan ke PT Digital Solutions', + reference: 'RCV-006-2024', + total: 8900000 + }, + { + date: '2024-09-12', + transaction: 'Piutang dari Toko Elektronik Prima', + reference: 'RCV-007-2024', + total: 2450000 + }, + { + date: '2024-09-15', + transaction: 'Invoice Penjualan Software License', + reference: 'RCV-008-2024', + total: 12500000 + }, + { + date: '2024-09-18', + transaction: 'Tagihan Jasa Maintenance Equipment', + reference: 'RCV-009-2024', + total: 3600000 + }, + { + date: '2024-09-20', + transaction: 'Piutang dari CV Kreatif Media', + reference: 'RCV-010-2024', + total: 1950000 + }, + { + date: '2024-09-22', + transaction: 'Invoice Jual Material Konstruksi', + reference: 'RCV-011-2024', + total: 7200000 + }, + { + date: '2024-09-25', + transaction: 'Tagihan Jasa Pelatihan IT', + reference: 'RCV-012-2024', + total: 4800000 + }, + { + date: '2024-09-28', + transaction: 'Piutang dari PT Logistik Nusantara', + reference: 'RCV-013-2024', + total: 5300000 + }, + { + date: '2024-09-30', + transaction: 'Invoice Penjualan Peralatan Kantor', + reference: 'RCV-014-2024', + total: 2100000 + }, + { + date: '2024-10-02', + transaction: 'Tagihan ke Supplier Packaging', + reference: 'RCV-015-2024', + total: 3850000 + } +] + +export const vendorTransactionData: VendorTransactionType[] = [ + { + date: '2024-08-15', + transaction: 'Pembelian Bahan Baku Produksi', + none: 'PO-001-2024', + total: 4500000 + }, + { + date: '2024-08-18', + transaction: 'Pembayaran Invoice Supplier', + none: 'PAY-001-2024', + total: 2750000 + }, + { + date: '2024-08-22', + transaction: 'Penjualan Produk ke Klien', + none: 'INV-001-2024', + total: 6200000 + }, + { + date: '2024-08-25', + transaction: 'Pembelian Peralatan Kantor', + none: 'PO-002-2024', + total: 1850000 + }, + { + date: '2024-08-28', + transaction: 'Pembayaran Jasa Konsultasi', + none: 'PAY-002-2024', + total: 3200000 + }, + { + date: '2024-09-02', + transaction: 'Penjualan Software License', + none: 'INV-002-2024', + total: 8900000 + }, + { + date: '2024-09-05', + transaction: 'Pembelian Material Konstruksi', + none: 'PO-003-2024', + total: 12500000 + }, + { + date: '2024-09-08', + transaction: 'Pembayaran Maintenance Equipment', + none: 'PAY-003-2024', + total: 2450000 + }, + { + date: '2024-09-12', + transaction: 'Penjualan Jasa Pelatihan', + none: 'INV-003-2024', + total: 4800000 + }, + { + date: '2024-09-15', + transaction: 'Pembelian Bahan Kimia', + none: 'PO-004-2024', + total: 3600000 + }, + { + date: '2024-09-18', + transaction: 'Pembayaran Sewa Gedung', + none: 'PAY-004-2024', + total: 15000000 + }, + { + date: '2024-09-22', + transaction: 'Penjualan Peralatan Elektronik', + none: 'INV-004-2024', + total: 7200000 + }, + { + date: '2024-09-25', + transaction: 'Pembelian Packaging Materials', + none: 'PO-005-2024', + total: 1950000 + }, + { + date: '2024-09-28', + transaction: 'Pembayaran Jasa Logistik', + none: 'PAY-005-2024', + total: 2100000 + }, + { + date: '2024-10-01', + transaction: 'Penjualan Produk Digital', + none: 'INV-005-2024', + total: 5300000 + } +] diff --git a/src/types/apps/vendorTypes.ts b/src/types/apps/vendorTypes.ts index 33de981..bc3b109 100644 --- a/src/types/apps/vendorTypes.ts +++ b/src/types/apps/vendorTypes.ts @@ -8,3 +8,16 @@ export type VendorType = { youPayable: number theyPayable: number } + +export type VendorDebsPayedType = { + date: string + transaction: string + reference: string + total: number +} +export type VendorTransactionType = { + date: string + transaction: string + none: string + total: number +} diff --git a/src/views/apps/vendor/detail/index.tsx b/src/views/apps/vendor/detail/index.tsx new file mode 100644 index 0000000..3869930 --- /dev/null +++ b/src/views/apps/vendor/detail/index.tsx @@ -0,0 +1,20 @@ +import VendorOverview from '@/views/apps/vendor/detail/vendor-overview' +import Grid from '@mui/material/Grid2' +import VendorContent from './vendor-content' + +const VendorView = async () => { + // Vars + + return ( + + + + + + + + + ) +} + +export default VendorView diff --git a/src/views/apps/vendor/detail/vendor-content/VendorMoneyInOutChart.tsx b/src/views/apps/vendor/detail/vendor-content/VendorMoneyInOutChart.tsx new file mode 100644 index 0000000..c521fd5 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/VendorMoneyInOutChart.tsx @@ -0,0 +1,157 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const colors = { + uangMasuk: '#28C76F', + uangKeluar: '#EA5455' +} + +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Uang Masuk', + type: 'column', + data: [850, 920, 780, 1150, 980, 1250, 1080, 950, 1300, 1100, 890, 1400] + }, + { + name: 'Uang Keluar', + type: 'column', + data: [650, 720, 580, 850, 780, 950, 880, 750, 1000, 900, 690, 1100] + } +] + +const VendorMoneyInOutChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + stacked: false, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + '.000' + } + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '60%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 5, + max: 1500, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + return 'Rp ' + val + 'rb' + } + } + }, + legend: { + markers: { + width: 8, + height: 8, + offsetX: -3, + radius: 12 + }, + height: 33, + offsetY: 10, + itemMargin: { + horizontal: 10, + vertical: 0 + }, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400, + labels: { + colors: bodyColor, + useSeriesColors: false + } + }, + grid: { + borderColor, + strokeDashArray: 6 + }, + colors: [colors.uangMasuk, colors.uangKeluar], + fill: { + opacity: 1 + } + } + + return ( + + } /> + + + + + ) +} + +export default VendorMoneyInOutChart diff --git a/src/views/apps/vendor/detail/vendor-content/VendorSalesChart.tsx b/src/views/apps/vendor/detail/vendor-content/VendorSalesChart.tsx new file mode 100644 index 0000000..6d8f3aa --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/VendorSalesChart.tsx @@ -0,0 +1,144 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const colors = { + penjualan: '#7367F0' +} + +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Penjualan', + type: 'column', + data: [1250, 1420, 980, 1650, 1380, 1750, 1580, 1350, 1900, 1600, 1290, 2100] + } +] + +const VendorSalesChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + stacked: false, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + '.000' + } + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '50%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 5, + max: 2200, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + return 'Rp ' + val + 'rb' + } + } + }, + legend: { + markers: { + width: 8, + height: 8, + offsetX: -3, + radius: 12 + }, + height: 33, + offsetY: 10, + itemMargin: { + horizontal: 10, + vertical: 0 + }, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400, + labels: { + colors: bodyColor, + useSeriesColors: false + } + }, + grid: { + borderColor, + strokeDashArray: 6 + }, + colors: [colors.penjualan], + fill: { + opacity: 1 + } + } + + return ( + + } /> + + + + + ) +} + +export default VendorSalesChart diff --git a/src/views/apps/vendor/detail/vendor-content/VendorStatistic.tsx b/src/views/apps/vendor/detail/vendor-content/VendorStatistic.tsx new file mode 100644 index 0000000..790430f --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/VendorStatistic.tsx @@ -0,0 +1,63 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Types Imports +import type { CardStatsHorizontalWithAvatarProps } from '@/types/pages/widgetTypes' + +// Component Imports +import CardStatsHorizontalWithAvatar from '@components/card-statistics/HorizontalWithAvatar' + +const data: CardStatsHorizontalWithAvatarProps[] = [ + { + stats: 'Rp 542.340.000', + title: 'Hutang Anda', + avatarIcon: 'tabler-credit-card', + avatarColor: 'error' + }, + { + stats: 'Rp 387.250.000', + title: 'Piutang Anda', + avatarIcon: 'tabler-wallet', + avatarColor: 'success' + }, + { + stats: 'Rp 156.800.000', + title: 'Pembayaran Diterima', + avatarIcon: 'tabler-arrow-down-circle', + avatarColor: 'success' + }, + { + stats: 'Rp 89.450.000', + title: 'Pembayaran Dikirim', + avatarIcon: 'tabler-arrow-up-circle', + avatarColor: 'warning' + }, + { + stats: 'Rp 67.890.000', + title: 'Hutang Jatuh Tempo', + avatarIcon: 'tabler-clock-exclamation', + avatarColor: 'error' + }, + { + stats: 'Rp 134.560.000', + title: 'Piutang Tertunda', + avatarIcon: 'tabler-clock-dollar', + avatarColor: 'info' + } +] + +const VendorStatistic = () => { + return ( + data && ( + + {data.map((item, index) => ( + + + + ))} + + ) + ) +} + +export default VendorStatistic diff --git a/src/views/apps/vendor/detail/vendor-content/index.tsx b/src/views/apps/vendor/detail/vendor-content/index.tsx new file mode 100644 index 0000000..c57eeb7 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/index.tsx @@ -0,0 +1,21 @@ +import VendorDebsPayed from './vendor-debs-payed-table' +import VendorReceivablesPayment from './vendor-receivables-payment-table' +import VendorTransaction from './vendor-transaction-table' +import VendorMoneyInOutChart from './VendorMoneyInOutChart' +import VendorSalesChart from './VendorSalesChart' +import VendorStatistic from './VendorStatistic' + +const VendorContent = () => { + return ( + <> + + + + + + + + ) +} + +export default VendorContent diff --git a/src/views/apps/vendor/detail/vendor-content/styles.css b/src/views/apps/vendor/detail/vendor-content/styles.css new file mode 100644 index 0000000..6d24cd4 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/styles.css @@ -0,0 +1,7 @@ +#keluar-masuk-uang .apexcharts-legend .apexcharts-legend-series { + border: 1px solid var(--mui-palette-divider); + border-radius: var(--mui-shape-borderRadius); + block-size: 83%; + padding-block: 4px; + padding-inline: 16px; +} diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/VendorDebsPayedTable.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/VendorDebsPayedTable.tsx new file mode 100644 index 0000000..2bb0986 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/VendorDebsPayedTable.tsx @@ -0,0 +1,334 @@ +'use client' + +// React Imports +import { useEffect, useState, useMemo } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' + +// Third-party Imports +import classnames from 'classnames' +import { rankItem } from '@tanstack/match-sorter-utils' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFacetedMinMaxValues, + getPaginationRowModel, + getSortedRowModel +} from '@tanstack/react-table' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// Type Imports +import type { ThemeColor } from '@core/types' +import type { Locale } from '@configs/i18n' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' + +// Util Imports +import { getLocalizedUrl } from '@/utils/i18n' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { formatCurrency } from '@/utils/transform' + +// Type Definition +export type VendorDebsPayedType = { + date: string + transaction: string + reference: string + total: number +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type VendorDebsPayedTypeWithAction = VendorDebsPayedType & { + action?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const VendorDebsPayedTable = ({ tableData }: { tableData?: VendorDebsPayedType[] }) => { + // States + const [rowSelection, setRowSelection] = useState({}) + const [data, setData] = useState(...[tableData]) + const [filteredData, setFilteredData] = useState(data) + const [globalFilter, setGlobalFilter] = useState('') + + // Hooks + const { lang: locale } = useParams() + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('transaction', { + header: 'Keterangan Transaksi', + cell: ({ row }) => ( +
+ + {row.original.transaction} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => + }), + columnHelper.accessor('total', { + header: () =>
Jumlah Hutang
, + cell: ({ row }) => ( +
+ + {formatCurrency(row.original.total)} + +
+ ) + }) + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, filteredData] + ) + + const table = useReactTable({ + data: filteredData as VendorDebsPayedType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + globalFilter + }, + initialState: { + pagination: { + pageSize: 10 + } + }, + enableRowSelection: true, + globalFilterFn: fuzzyFilter, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + onGlobalFilterChange: setGlobalFilter, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getFacetedMinMaxValues: getFacetedMinMaxValues() + }) + + // Calculate total debt + const totalDebt = useMemo(() => { + return filteredData?.reduce((sum, item) => sum + item.total, 0) || 0 + }, [filteredData]) + + return ( + <> + + +
+ table.setPageSize(Number(e.target.value))} + className='max-sm:is-full sm:is-[70px]' + > + 10 + 25 + 50 + +
+ setGlobalFilter(String(value))} + placeholder='Cari Hutang' + className='max-sm:is-full' + /> + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+
+ + + Tidak ada hutang ditemukan + + + Cobalah mengubah filter pencarian + +
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ { + table.setPageIndex(page) + }} + /> +
+ + ) +} + +export default VendorDebsPayedTable diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/index.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/index.tsx new file mode 100644 index 0000000..35c3ce0 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-debs-payed-table/index.tsx @@ -0,0 +1,21 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { VendorType } from '@/types/apps/vendorTypes' + +// Component Imports +import VendorDebsPayedTable from './VendorDebsPayedTable' +import { vendorDebsPayedData } from '@/data/dummy/vendor' + +const VendorDebsPayed = () => { + return ( + + + + + + ) +} + +export default VendorDebsPayed diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/VendorReceivablesPaymentTable.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/VendorReceivablesPaymentTable.tsx new file mode 100644 index 0000000..5ddc36a --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/VendorReceivablesPaymentTable.tsx @@ -0,0 +1,327 @@ +'use client' + +// React Imports +import { useEffect, useState, useMemo } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' + +// Third-party Imports +import classnames from 'classnames' +import { rankItem } from '@tanstack/match-sorter-utils' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFacetedMinMaxValues, + getPaginationRowModel, + getSortedRowModel +} from '@tanstack/react-table' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// Type Imports +import type { ThemeColor } from '@core/types' +import type { Locale } from '@configs/i18n' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' + +// Util Imports +import { getLocalizedUrl } from '@/utils/i18n' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { formatCurrency } from '@/utils/transform' +import { VendorDebsPayedType } from '../vendor-debs-payed-table/VendorDebsPayedTable' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type VendorDebsPayedTypeWithAction = VendorDebsPayedType & { + action?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const VendorReceivablesPaymentTable = ({ tableData }: { tableData?: VendorDebsPayedType[] }) => { + // States + const [rowSelection, setRowSelection] = useState({}) + const [data, setData] = useState(...[tableData]) + const [filteredData, setFilteredData] = useState(data) + const [globalFilter, setGlobalFilter] = useState('') + + // Hooks + const { lang: locale } = useParams() + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('transaction', { + header: 'Keterangan Transaksi', + cell: ({ row }) => ( +
+ + {row.original.transaction} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => + }), + columnHelper.accessor('total', { + header: () =>
Jumlah Hutang
, + cell: ({ row }) => ( +
+ + {formatCurrency(row.original.total)} + +
+ ) + }) + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, filteredData] + ) + + const table = useReactTable({ + data: filteredData as VendorDebsPayedType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + globalFilter + }, + initialState: { + pagination: { + pageSize: 10 + } + }, + enableRowSelection: true, + globalFilterFn: fuzzyFilter, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + onGlobalFilterChange: setGlobalFilter, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getFacetedMinMaxValues: getFacetedMinMaxValues() + }) + + // Calculate total debt + const totalDebt = useMemo(() => { + return filteredData?.reduce((sum, item) => sum + item.total, 0) || 0 + }, [filteredData]) + + return ( + <> + + +
+ table.setPageSize(Number(e.target.value))} + className='max-sm:is-full sm:is-[70px]' + > + 10 + 25 + 50 + +
+ setGlobalFilter(String(value))} + placeholder='Cari' + className='max-sm:is-full' + /> + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+
+ + + Tidak ada hutang ditemukan + + + Cobalah mengubah filter pencarian + +
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ { + table.setPageIndex(page) + }} + /> +
+ + ) +} + +export default VendorReceivablesPaymentTable diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/index.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/index.tsx new file mode 100644 index 0000000..fbf21df --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-receivables-payment-table/index.tsx @@ -0,0 +1,18 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Component Imports +import VendorReceivablesPaymentTable from './VendorReceivablesPaymentTable' +import { vendorReceivablesData } from '@/data/dummy/vendor' + +const VendorReceivablesPayment = () => { + return ( + + + + + + ) +} + +export default VendorReceivablesPayment diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/VendorTransactionTable.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/VendorTransactionTable.tsx new file mode 100644 index 0000000..e9895d1 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/VendorTransactionTable.tsx @@ -0,0 +1,355 @@ +'use client' + +// React Imports +import { useEffect, useState, useMemo } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' + +// Third-party Imports +import classnames from 'classnames' +import { rankItem } from '@tanstack/match-sorter-utils' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFacetedMinMaxValues, + getPaginationRowModel, + getSortedRowModel +} from '@tanstack/react-table' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// Type Imports +import type { ThemeColor } from '@core/types' +import type { Locale } from '@configs/i18n' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' + +// Util Imports +import { getLocalizedUrl } from '@/utils/i18n' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { formatCurrency } from '@/utils/transform' + +// Type Definition +export type VendorTransactionType = { + date: string + transaction: string + none: string + total: number +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type VendorTransactionTypeWithAction = VendorTransactionType & { + action?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Format date helper +const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const VendorTransactionTable = ({ tableData }: { tableData?: VendorTransactionType[] }) => { + // States + const [rowSelection, setRowSelection] = useState({}) + const [data, setData] = useState(...[tableData]) + const [filteredData, setFilteredData] = useState(data) + const [globalFilter, setGlobalFilter] = useState('') + + // Hooks + const { lang: locale } = useParams() + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('transaction', { + header: 'Keterangan Transaksi', + cell: ({ row }) => ( +
+ + {row.original.transaction} + +
+ ) + }), + columnHelper.accessor('none', { + header: 'Referensi', + cell: ({ row }) => + }), + columnHelper.accessor('total', { + header: () =>
Jumlah
, + cell: ({ row }) => ( +
+ + {formatCurrency(row.original.total)} + +
+ ) + }) + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [data, filteredData] + ) + + const table = useReactTable({ + data: filteredData as VendorTransactionType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + globalFilter + }, + initialState: { + pagination: { + pageSize: 10 + } + }, + enableRowSelection: true, + globalFilterFn: fuzzyFilter, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + onGlobalFilterChange: setGlobalFilter, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getFacetedMinMaxValues: getFacetedMinMaxValues() + }) + + // Calculate total amount + const totalAmount = useMemo(() => { + return filteredData?.reduce((sum, item) => sum + item.total, 0) || 0 + }, [filteredData]) + + return ( + <> + + + } + /> +
+ table.setPageSize(Number(e.target.value))} + className='max-sm:is-full sm:is-[70px]' + > + 10 + 25 + 50 + +
+ setGlobalFilter(String(value))} + placeholder='Cari Transaksi' + className='max-sm:is-full' + /> + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+
+ + + Tidak ada transaksi ditemukan + + + Cobalah mengubah filter pencarian + +
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ { + table.setPageIndex(page) + }} + /> +
+ + ) +} + +export default VendorTransactionTable diff --git a/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/index.tsx b/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/index.tsx new file mode 100644 index 0000000..e492e2a --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-content/vendor-transaction-table/index.tsx @@ -0,0 +1,18 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Component Imports +import { vendorTransactionData } from '@/data/dummy/vendor' +import VendorTransactionTable from './VendorTransactionTable' + +const VendorTransaction = () => { + return ( + + + + + + ) +} + +export default VendorTransaction diff --git a/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx b/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx new file mode 100644 index 0000000..f0f4615 --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-overview/VendorDetails.tsx @@ -0,0 +1,134 @@ +// MUI Imports +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Divider from '@mui/material/Divider' +import Button from '@mui/material/Button' +import type { ButtonProps } from '@mui/material/Button' + +// Type Imports +import type { ThemeColor } from '@core/types' + +// Component Imports +import EditUserInfo from '@components/dialogs/edit-user-info' +import ConfirmationDialog from '@components/dialogs/confirmation-dialog' +import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick' +import CustomAvatar from '@core/components/mui/Avatar' + +// Vars +const userData = { + firstName: 'Wadi Adika Adrian', + lastName: 'Firgantoro', + userName: '@wadiAdika', + perusahaan: 'Yayasan Pertiwi Tb', + email: 'labuh87@napitupu', + status: 'active', + role: 'Vendor', + telepon: '622301190560', + alamatPenagihan: 'Gg. Sentot Alibasa 381, Cimahi 66726 Sulbar', + akunHutang: '2-20100 Hutang U', + akunPiutang: '', + kenaPajak: 'Kena Pajak' +} + +const VendorDetails = () => { + // Vars + const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({ + children, + color, + variant + }) + + return ( + <> + + +
+
+
+ + {`${userData.firstName} ${userData.lastName}`} +
+ +
+
+ + {/* Detail Kontak Section */} +
+ Detail Kontak + +
+
+ + Nama: + + {`${userData.firstName} ${userData.lastName}`} +
+
+ + Perusahaan: + + {userData.perusahaan} +
+
+ + Email: + + + {userData.email} + +
+
+ + Telepon: + + + {userData.telepon} + +
+
+ + Alamat Penagihan: + + + {userData.alamatPenagihan} + +
+
+
+ + {/* Pemetaan Akun Section */} +
+ Pemetaan Akun + +
+
+ + Akun Hutang: + + + {userData.akunHutang} + +
+
+ + Akun Piutang: + + {userData.akunPiutang || '-'} +
+
+ + Kena Pajak: + + {userData.kenaPajak} +
+
+
+
+
+ + ) +} + +export default VendorDetails diff --git a/src/views/apps/vendor/detail/vendor-overview/index.tsx b/src/views/apps/vendor/detail/vendor-overview/index.tsx new file mode 100644 index 0000000..f59240e --- /dev/null +++ b/src/views/apps/vendor/detail/vendor-overview/index.tsx @@ -0,0 +1,17 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Component Imports +import VendorDetails from './VendorDetails' + +const VendorOverview = () => { + return ( + + + + + + ) +} + +export default VendorOverview diff --git a/src/views/apps/vendor/list/VendorListTable.tsx b/src/views/apps/vendor/list/VendorListTable.tsx index 284b160..754d059 100644 --- a/src/views/apps/vendor/list/VendorListTable.tsx +++ b/src/views/apps/vendor/list/VendorListTable.tsx @@ -161,7 +161,7 @@ const VendorListTable = ({ tableData }: { tableData?: VendorType[] }) => {
{getAvatar({ photo: row.original.photo, name: row.original.name })}
- + {row.original.name} From 5dc62b83907dcc715fb46ba581ac7135891218ce Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 15:42:05 +0700 Subject: [PATCH 06/42] Sales Overview Page --- .../(private)/apps/sales/overview/page.tsx | 86 +++++++ .../layout/vertical/VerticalMenu.tsx | 4 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- .../overview-left/SalesCustomerSalesChart.tsx | 187 +++++++++++++++ .../SalesPaymentRatioGaugeChart.tsx | 104 +++++++++ .../overview-left/SalesProductChart.tsx | 218 ++++++++++++++++++ .../sales/overview/overview-left/index.tsx | 15 ++ .../overview-right/SalesBillPaymentChart.tsx | 157 +++++++++++++ .../overview-right/SalesOverviewCards.tsx | 62 +++++ .../SalesPaymentReceivedChart.tsx | 152 ++++++++++++ .../overview-right/SalesPerPersonChart.tsx | 174 ++++++++++++++ .../sales/overview/overview-right/index.tsx | 17 ++ .../sales/overview/overview-right/styles.css | 15 ++ 14 files changed, 1195 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx create mode 100644 src/views/apps/sales/overview/overview-left/SalesCustomerSalesChart.tsx create mode 100644 src/views/apps/sales/overview/overview-left/SalesPaymentRatioGaugeChart.tsx create mode 100644 src/views/apps/sales/overview/overview-left/SalesProductChart.tsx create mode 100644 src/views/apps/sales/overview/overview-left/index.tsx create mode 100644 src/views/apps/sales/overview/overview-right/SalesBillPaymentChart.tsx create mode 100644 src/views/apps/sales/overview/overview-right/SalesOverviewCards.tsx create mode 100644 src/views/apps/sales/overview/overview-right/SalesPaymentReceivedChart.tsx create mode 100644 src/views/apps/sales/overview/overview-right/SalesPerPersonChart.tsx create mode 100644 src/views/apps/sales/overview/overview-right/index.tsx create mode 100644 src/views/apps/sales/overview/overview-right/styles.css diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx new file mode 100644 index 0000000..dcdbea9 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx @@ -0,0 +1,86 @@ +'use client' + +import Grid from '@mui/material/Grid2' + +import { TextField, Typography, useTheme } from '@mui/material' +import { useState } from 'react' +import { formatDateDDMMYYYY, formatForInputDate } from '@/utils/transform' +import SalesOverviewRight from '@/views/apps/sales/overview/overview-right' +import SalesOverviewLeft from '@/views/apps/sales/overview/overview-left' + +const SalesOverviewPage = () => { + const theme = useTheme() + + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const [filter, setFilter] = useState({ + date_from: formatDateDDMMYYYY(monthAgo), + date_to: formatDateDDMMYYYY(today) + }) + + return ( + <> + + +
+ + Orders Analysis Dashboard + +
+ { + setFilter({ + ...filter, + date_from: formatDateDDMMYYYY(new Date(e.target.value)) + }) + }} + size='small' + sx={{ + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: 'primary.main' + }, + '& fieldset': { + borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider + } + } + }} + /> + - + {}} + size='small' + sx={{ + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: 'primary.main' + }, + '& fieldset': { + borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider + } + } + }} + /> +
+
+
+
+ + + + + + + + + + ) +} + +export default SalesOverviewPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 648248d..59a197e 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -91,6 +91,10 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].dailyReport} + }> + {dictionary['navigation'].overview} + {/* {dictionary['navigation'].view} */} + }> {dictionary['navigation'].list} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 9904156..30b4549 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -112,6 +112,7 @@ "menuLevel3": "Menu Level 3", "disabledMenu": "Disabled Menu", "dailyReport": "Daily Report", - "vendor": "Vendor" + "vendor": "Vendor", + "sales": "Sales" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index af683c1..6dd0497 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -112,6 +112,7 @@ "menuLevel3": "Level Menu 3", "disabledMenu": "Menu Nonaktif", "dailyReport": "Laporan Harian", - "vendor": "Vendor" + "vendor": "Vendor", + "sales": "Penjualan" } } diff --git a/src/views/apps/sales/overview/overview-left/SalesCustomerSalesChart.tsx b/src/views/apps/sales/overview/overview-left/SalesCustomerSalesChart.tsx new file mode 100644 index 0000000..0b51a29 --- /dev/null +++ b/src/views/apps/sales/overview/overview-left/SalesCustomerSalesChart.tsx @@ -0,0 +1,187 @@ +'use client' + +// React Imports +import { useState } from 'react' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Button from '@mui/material/Button' +import ButtonGroup from '@mui/material/ButtonGroup' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import TableFooter from '@mui/material/TableFooter' +import Typography from '@mui/material/Typography' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Data +const customerData = [ + { pelanggan: 'Mumpuni Imam Rajata...', perusahaan: 'Fa Hassanah', nilai: 14117300 }, + { pelanggan: 'Balangga Saefullah M...', perusahaan: 'CV Siregar Prasetya Tbk', nilai: 12520640 }, + { pelanggan: 'Jais Mursita Adrians...', perusahaan: 'UD Sinaga Waluyo', nilai: 12174120 }, + { pelanggan: 'Paramita Wijayanti M...', perusahaan: 'PD Waluyo Yuniar', nilai: 11001060 }, + { pelanggan: 'Putri Puji Pertiwi S...', perusahaan: 'CV Wahyudin Tbk', nilai: 9175560 }, + { pelanggan: 'Lainnya', perusahaan: '', nilai: 63285770 } +] + +const totalNilai = customerData.reduce((sum, item) => sum + item.nilai, 0) + +// Chart series and colors +const series = customerData.map(item => item.nilai) +const colors = ['#FF6B9D', '#FFD93D', '#6BCF7F', '#4D96FF', '#B39DDB', '#E91E63'] + +const SalesCustomerSalesChart = () => { + const [activeView, setActiveView] = useState<'chart' | 'table'>('chart') + + const options: ApexOptions = { + chart: { + type: 'donut' + }, + labels: customerData.map(item => item.pelanggan), + colors: colors, + legend: { + position: 'bottom', + horizontalAlign: 'center', + fontSize: '14px', + markers: { + width: 12, + height: 12, + radius: 2 + }, + itemMargin: { + horizontal: 15, + vertical: 5 + } + }, + plotOptions: { + pie: { + donut: { + size: '70%' + } + } + }, + dataLabels: { + enabled: false + }, + tooltip: { + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + stroke: { + width: 2, + colors: ['#fff'] + } + } + + const formatCurrency = (amount: number): string => { + return amount.toLocaleString('id-ID') + } + + return ( + + +
+ +
+ } + /> + + {/* Content based on active view */} + {activeView === 'chart' ? ( + // Donut Chart View (Slide 1) +
+ +
+ ) : ( + // Table View (Slide 2) + + + + + Pelanggan + + Nilai + + + + + {customerData.map((row, index) => ( + + +
+ + {row.pelanggan} + + {row.perusahaan && ( + + {row.perusahaan} + + )} +
+
+ {formatCurrency(row.nilai)} +
+ ))} +
+ + + Total + + {formatCurrency(totalNilai)} + + + +
+
+ )} + + {/* Two dot indicators at bottom */} +
+
setActiveView('chart')} + >
+
setActiveView('table')} + >
+
+
+ + ) +} + +export default SalesCustomerSalesChart diff --git a/src/views/apps/sales/overview/overview-left/SalesPaymentRatioGaugeChart.tsx b/src/views/apps/sales/overview/overview-left/SalesPaymentRatioGaugeChart.tsx new file mode 100644 index 0000000..23a5540 --- /dev/null +++ b/src/views/apps/sales/overview/overview-left/SalesPaymentRatioGaugeChart.tsx @@ -0,0 +1,104 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars +const series = [52.2] + +const SalesPaymentRatioGaugeChart = () => { + const options: ApexOptions = { + chart: { + type: 'radialBar', + sparkline: { + enabled: true + } + }, + plotOptions: { + radialBar: { + startAngle: -90, + endAngle: 90, + track: { + background: '#e7e7e7', + strokeWidth: '97%', + margin: 5, + dropShadow: { + enabled: false + } + }, + hollow: { + margin: 15, + size: '70%' + }, + dataLabels: { + name: { + show: false + }, + value: { + offsetY: -10, + fontSize: '32px', + fontWeight: 'bold', + color: '#1f2937', + formatter: function (val) { + return val + '%' + } + } + } + } + }, + grid: { + padding: { + top: -10 + } + }, + fill: { + type: 'gradient', + gradient: { + shade: 'light', + shadeIntensity: 0.4, + inverseColors: false, + opacityFrom: 1, + opacityTo: 1, + stops: [0, 50, 53, 91] + } + }, + colors: ['#3b82f6'], + stroke: { + dashArray: 4 + }, + labels: ['Rasio Lunas'] + } + + return ( + + } /> + + +
Tagihan lunas vs total Tagihan tahun ini
+
+
+ ) +} + +export default SalesPaymentRatioGaugeChart diff --git a/src/views/apps/sales/overview/overview-left/SalesProductChart.tsx b/src/views/apps/sales/overview/overview-left/SalesProductChart.tsx new file mode 100644 index 0000000..237be40 --- /dev/null +++ b/src/views/apps/sales/overview/overview-left/SalesProductChart.tsx @@ -0,0 +1,218 @@ +'use client' + +// React Imports +import { useState } from 'react' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Button from '@mui/material/Button' +import ButtonGroup from '@mui/material/ButtonGroup' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import TableFooter from '@mui/material/TableFooter' +import Typography from '@mui/material/Typography' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Data +const productData = [ + { kategori: 'Dress', jumlah: 174, nilai: 70629279 }, + { kategori: 'Tshirt', jumlah: 163, nilai: 31667892 }, + { kategori: 'Shoes', jumlah: 27, nilai: 13473000 }, + { kategori: 'Misc', jumlah: 128, nilai: 123937 } +] + +const totalJumlah = productData.reduce((sum, item) => sum + item.jumlah, 0) +const totalNilai = productData.reduce((sum, item) => sum + item.nilai, 0) + +// Chart series and colors +const series = productData.map(item => item.nilai) +const colors = ['#FF6B9D', '#FFD93D', '#6BCF7F', '#4D96FF'] + +const SalesProductSalesChart = () => { + const [activeView, setActiveView] = useState<'chart' | 'table'>('chart') + + const options: ApexOptions = { + chart: { + type: 'donut' + }, + labels: productData.map(item => item.kategori), + colors: colors, + legend: { + position: 'bottom', + horizontalAlign: 'center', + fontSize: '14px', + markers: { + width: 12, + height: 12, + radius: 2 + }, + itemMargin: { + horizontal: 15, + vertical: 5 + } + }, + plotOptions: { + pie: { + donut: { + size: '70%' + } + } + }, + dataLabels: { + enabled: false + }, + tooltip: { + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + stroke: { + width: 2, + colors: ['#fff'] + } + } + + const formatCurrency = (amount: number): string => { + return amount.toLocaleString('id-ID') + } + + return ( + + +
+ +
+ } + /> + + {/* Toggle Buttons */} +
+ + + + +
+ + {/* Content based on active view */} + {activeView === 'chart' ? ( + // Donut Chart View (Slide 1) +
+ +
+ ) : ( + // Table View (Slide 2) + + + + + Kategori + + Jml + + + Nilai + + + + + {productData.map((row, index) => ( + + {row.kategori} + {row.jumlah} + {formatCurrency(row.nilai)} + + ))} + + + + Total + + {totalJumlah} + + + {formatCurrency(totalNilai)} + + + +
+
+ )} + + {/* Two dot indicators at bottom */} +
+
setActiveView('chart')} + >
+
setActiveView('table')} + >
+
+
+ + ) +} + +export default SalesProductSalesChart diff --git a/src/views/apps/sales/overview/overview-left/index.tsx b/src/views/apps/sales/overview/overview-left/index.tsx new file mode 100644 index 0000000..f87a461 --- /dev/null +++ b/src/views/apps/sales/overview/overview-left/index.tsx @@ -0,0 +1,15 @@ +import SalesCustomerSalesChart from './SalesCustomerSalesChart' +import SalesPaymentRatioGaugeChart from './SalesPaymentRatioGaugeChart' +import SalesProductSalesChart from './SalesProductChart' + +const SalesOverviewLeft = () => { + return ( + <> + + + + + ) +} + +export default SalesOverviewLeft diff --git a/src/views/apps/sales/overview/overview-right/SalesBillPaymentChart.tsx b/src/views/apps/sales/overview/overview-right/SalesBillPaymentChart.tsx new file mode 100644 index 0000000..ccd9389 --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/SalesBillPaymentChart.tsx @@ -0,0 +1,157 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const colors = { + tagihan: '#28C76F', + pembayaran: '#EA5455' +} + +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Tagihan', + type: 'column', + data: [1250, 1180, 1350, 1450, 1200, 1520, 1380, 1150, 1650, 1400, 1290, 1700] + }, + { + name: 'Pembayaran', + type: 'column', + data: [950, 880, 1050, 1150, 900, 1220, 1080, 850, 1350, 1100, 990, 1400] + } +] + +const SalesBillPaymentChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + stacked: false, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + '.000' + } + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '60%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 5, + max: 1800, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + return 'Rp ' + val + 'rb' + } + } + }, + legend: { + markers: { + width: 8, + height: 8, + offsetX: -3, + radius: 12 + }, + height: 33, + offsetY: 10, + itemMargin: { + horizontal: 10, + vertical: 0 + }, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400, + labels: { + colors: bodyColor, + useSeriesColors: false + } + }, + grid: { + borderColor, + strokeDashArray: 6 + }, + colors: [colors.tagihan, colors.pembayaran], + fill: { + opacity: 1 + } + } + + return ( + + } /> + + + + + ) +} + +export default SalesBillPaymentChart diff --git a/src/views/apps/sales/overview/overview-right/SalesOverviewCards.tsx b/src/views/apps/sales/overview/overview-right/SalesOverviewCards.tsx new file mode 100644 index 0000000..0107660 --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/SalesOverviewCards.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'PENJUALAN', + stats: '122.274.450', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '162,3%', + subtitle: '46 Tagihan Tahun Ini' + }, + { + title: 'PEMBAYARAN DITERIMA', + stats: '66.742.440', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '175,6%', + subtitle: '27 Tagihan Tahun Ini' + }, + { + title: 'MENUNGGU PEMBAYARAN', + stats: '55.532.009', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '149,6%', + subtitle: '22 Tagihan' + }, + { + title: 'JATUH TEMPO', + stats: '48.246.990', + avatarIcon: 'tabler-clock-exclamation', + avatarColor: 'error', + trend: 'negative', + trendNumber: '89,9%', + subtitle: '16 Tagihan' + } +] + +const SalesOverviewCards = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default SalesOverviewCards diff --git a/src/views/apps/sales/overview/overview-right/SalesPaymentReceivedChart.tsx b/src/views/apps/sales/overview/overview-right/SalesPaymentReceivedChart.tsx new file mode 100644 index 0000000..81977b5 --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/SalesPaymentReceivedChart.tsx @@ -0,0 +1,152 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Pembayaran Diterima', + data: [0, 0, 0, 0, 0, 67000000] + } +] + +const SalesPaymentReceivedChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + stroke: { + width: 3, + curve: 'smooth' + }, + markers: { + size: 6, + colors: ['#00CFE8'], + strokeColors: '#fff', + strokeWidth: 2, + hover: { + size: 8 + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['2020', '2021', '2022', '2023', '2024', '2025'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 7, + max: 70000000, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + if (val === 0) return '0' + return (val / 1000000).toFixed(0) + '.000.000' + } + } + }, + legend: { + show: false + }, + grid: { + borderColor, + strokeDashArray: 6, + xaxis: { + lines: { + show: true + } + }, + yaxis: { + lines: { + show: true + } + } + }, + colors: ['#00CFE8'], + fill: { + opacity: 1 + } + } + + return ( + + +
+ + + } + /> + + + +
+ ) +} + +export default SalesPaymentReceivedChart diff --git a/src/views/apps/sales/overview/overview-right/SalesPerPersonChart.tsx b/src/views/apps/sales/overview/overview-right/SalesPerPersonChart.tsx new file mode 100644 index 0000000..7f55c1d --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/SalesPerPersonChart.tsx @@ -0,0 +1,174 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const colors = ['#EA5455', '#FFB830', '#28C76F', '#00CFE8', '#795548', '#E91E63'] + +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: '2020', + data: [1850] + }, + { + name: '2021', + data: [2150] + }, + { + name: '2022', + data: [2450] + }, + { + name: '2023', + data: [2750] + }, + { + name: '2024', + data: [3050] + }, + { + name: '2025', + data: [3350] + } +] + +const SalesPerPersonChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + '.000' + } + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '60%', + borderRadius: 6, + dataLabels: { + position: 'top' + } + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Jason Marc'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + } + }, + legend: { + markers: { + width: 8, + height: 8, + offsetX: -3, + radius: 12 + }, + height: 40, + offsetY: 10, + itemMargin: { + horizontal: 10, + vertical: 0 + }, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400, + labels: { + colors: bodyColor, + useSeriesColors: false + } + }, + grid: { + borderColor, + strokeDashArray: 6, + xaxis: { + lines: { + show: true + } + }, + yaxis: { + lines: { + show: false + } + } + }, + colors: colors, + fill: { + opacity: 1 + } + } + + return ( + + } /> + + + + + ) +} + +export default SalesPerPersonChart diff --git a/src/views/apps/sales/overview/overview-right/index.tsx b/src/views/apps/sales/overview/overview-right/index.tsx new file mode 100644 index 0000000..0418ba1 --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/index.tsx @@ -0,0 +1,17 @@ +import SalesBillPaymentChart from './SalesBillPaymentChart' +import SalesOverviewCards from './SalesOverviewCards' +import SalesPaymentReceivedChart from './SalesPaymentReceivedChart' +import SalesPerPersonChart from './SalesPerPersonChart' + +const SalesOverviewRight = () => { + return ( + <> + + + + + + ) +} + +export default SalesOverviewRight diff --git a/src/views/apps/sales/overview/overview-right/styles.css b/src/views/apps/sales/overview/overview-right/styles.css new file mode 100644 index 0000000..ec19295 --- /dev/null +++ b/src/views/apps/sales/overview/overview-right/styles.css @@ -0,0 +1,15 @@ +#tagihan-pembayaran .apexcharts-legend .apexcharts-legend-series { + border: 1px solid var(--mui-palette-divider); + border-radius: var(--mui-shape-borderRadius); + block-size: 83%; + padding-block: 4px; + padding-inline: 16px; +} + +#penjualan-per-sales-person .apexcharts-legend .apexcharts-legend-series { + border: 1px solid var(--mui-palette-divider); + border-radius: var(--mui-shape-borderRadius); + block-size: 83%; + padding-block: 4px; + padding-inline: 16px; +} From 849013a08ac70ede22cd0243eb3ba07c612e2245 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 16:17:25 +0700 Subject: [PATCH 07/42] Purchase Overview Page --- .../(private)/apps/purchase/overview/page.tsx | 86 +++++++ .../(private)/apps/sales/overview/page.tsx | 2 +- .../layout/vertical/VerticalMenu.tsx | 3 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- .../PurchasePaymentRatioGaugeChart.tsx | 106 +++++++++ .../overview-left/PurchaseProductChart.tsx | 218 ++++++++++++++++++ .../PurchaseVendorSalesChart.tsx | 187 +++++++++++++++ .../purchase/overview/overview-left/index.tsx | 15 ++ .../PurchaseBillPaymentChart.tsx | 160 +++++++++++++ .../overview-right/PurchaseOverviewCards.tsx | 62 +++++ .../PurchasePaymentReceivedChart.tsx | 152 ++++++++++++ .../overview/overview-right/index.tsx | 15 ++ .../overview/overview-right/styles.css | 15 ++ 14 files changed, 1024 insertions(+), 3 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/overview/page.tsx create mode 100644 src/views/apps/purchase/overview/overview-left/PurchasePaymentRatioGaugeChart.tsx create mode 100644 src/views/apps/purchase/overview/overview-left/PurchaseProductChart.tsx create mode 100644 src/views/apps/purchase/overview/overview-left/PurchaseVendorSalesChart.tsx create mode 100644 src/views/apps/purchase/overview/overview-left/index.tsx create mode 100644 src/views/apps/purchase/overview/overview-right/PurchaseBillPaymentChart.tsx create mode 100644 src/views/apps/purchase/overview/overview-right/PurchaseOverviewCards.tsx create mode 100644 src/views/apps/purchase/overview/overview-right/PurchasePaymentReceivedChart.tsx create mode 100644 src/views/apps/purchase/overview/overview-right/index.tsx create mode 100644 src/views/apps/purchase/overview/overview-right/styles.css diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/overview/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/overview/page.tsx new file mode 100644 index 0000000..88d3888 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/overview/page.tsx @@ -0,0 +1,86 @@ +'use client' + +import Grid from '@mui/material/Grid2' + +import { TextField, Typography, useTheme } from '@mui/material' +import { useState } from 'react' +import { formatDateDDMMYYYY, formatForInputDate } from '@/utils/transform' +import PurchaseOverviewLeft from '@/views/apps/purchase/overview/overview-left' +import PurchaseOverviewRight from '@/views/apps/purchase/overview/overview-right' + +const PurchaseOverviewPage = () => { + const theme = useTheme() + + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const [filter, setFilter] = useState({ + date_from: formatDateDDMMYYYY(monthAgo), + date_to: formatDateDDMMYYYY(today) + }) + + return ( + <> + + +
+ + Overview Pembelian + +
+ { + setFilter({ + ...filter, + date_from: formatDateDDMMYYYY(new Date(e.target.value)) + }) + }} + size='small' + sx={{ + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: 'primary.main' + }, + '& fieldset': { + borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider + } + } + }} + /> + - + {}} + size='small' + sx={{ + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: 'primary.main' + }, + '& fieldset': { + borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider + } + } + }} + /> +
+
+
+
+ + + + + + + + + + ) +} + +export default PurchaseOverviewPage diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx index dcdbea9..c8aa27e 100644 --- a/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/overview/page.tsx @@ -26,7 +26,7 @@ const SalesOverviewPage = () => {
- Orders Analysis Dashboard + Overview Penjualan
{ {dictionary['navigation'].overview} {/* {dictionary['navigation'].view} */} + }> + {dictionary['navigation'].overview} + }> {dictionary['navigation'].list} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 30b4549..c20fde3 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -113,6 +113,7 @@ "disabledMenu": "Disabled Menu", "dailyReport": "Daily Report", "vendor": "Vendor", - "sales": "Sales" + "sales": "Sales", + "purchase_text": "Purchase" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 6dd0497..7c4c7fe 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -113,6 +113,7 @@ "disabledMenu": "Menu Nonaktif", "dailyReport": "Laporan Harian", "vendor": "Vendor", - "sales": "Penjualan" + "sales": "Penjualan", + "purchase_text": "Pembelian" } } diff --git a/src/views/apps/purchase/overview/overview-left/PurchasePaymentRatioGaugeChart.tsx b/src/views/apps/purchase/overview/overview-left/PurchasePaymentRatioGaugeChart.tsx new file mode 100644 index 0000000..ce31584 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-left/PurchasePaymentRatioGaugeChart.tsx @@ -0,0 +1,106 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Vars +const series = [52.2] + +const PurchasePaymentRatioGaugeChart = () => { + const options: ApexOptions = { + chart: { + type: 'radialBar', + sparkline: { + enabled: true + } + }, + plotOptions: { + radialBar: { + startAngle: -90, + endAngle: 90, + track: { + background: '#e7e7e7', + strokeWidth: '97%', + margin: 5, + dropShadow: { + enabled: false + } + }, + hollow: { + margin: 15, + size: '70%' + }, + dataLabels: { + name: { + show: false + }, + value: { + offsetY: -10, + fontSize: '32px', + fontWeight: 'bold', + color: '#1f2937', + formatter: function (val) { + return val + '%' + } + } + } + } + }, + grid: { + padding: { + top: -10 + } + }, + fill: { + type: 'gradient', + gradient: { + shade: 'light', + shadeIntensity: 0.4, + inverseColors: false, + opacityFrom: 1, + opacityTo: 1, + stops: [0, 50, 53, 91] + } + }, + colors: ['#3b82f6'], + stroke: { + dashArray: 4 + }, + labels: ['Rasio Lunas'] + } + + return ( + + } /> + + +
+ Tagihan Pembelian lunas vs total Tagihan Pembelian bulan ini +
+
+
+ ) +} + +export default PurchasePaymentRatioGaugeChart diff --git a/src/views/apps/purchase/overview/overview-left/PurchaseProductChart.tsx b/src/views/apps/purchase/overview/overview-left/PurchaseProductChart.tsx new file mode 100644 index 0000000..589da6f --- /dev/null +++ b/src/views/apps/purchase/overview/overview-left/PurchaseProductChart.tsx @@ -0,0 +1,218 @@ +'use client' + +// React Imports +import { useState } from 'react' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Button from '@mui/material/Button' +import ButtonGroup from '@mui/material/ButtonGroup' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import TableFooter from '@mui/material/TableFooter' +import Typography from '@mui/material/Typography' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Data +const productData = [ + { kategori: 'Dress', jumlah: 174, nilai: 70629279 }, + { kategori: 'Tshirt', jumlah: 163, nilai: 31667892 }, + { kategori: 'Shoes', jumlah: 27, nilai: 13473000 }, + { kategori: 'Misc', jumlah: 128, nilai: 123937 } +] + +const totalJumlah = productData.reduce((sum, item) => sum + item.jumlah, 0) +const totalNilai = productData.reduce((sum, item) => sum + item.nilai, 0) + +// Chart series and colors +const series = productData.map(item => item.nilai) +const colors = ['#FF6B9D', '#FFD93D', '#6BCF7F', '#4D96FF'] + +const PurchaseProductSalesChart = () => { + const [activeView, setActiveView] = useState<'chart' | 'table'>('chart') + + const options: ApexOptions = { + chart: { + type: 'donut' + }, + labels: productData.map(item => item.kategori), + colors: colors, + legend: { + position: 'bottom', + horizontalAlign: 'center', + fontSize: '14px', + markers: { + width: 12, + height: 12, + radius: 2 + }, + itemMargin: { + horizontal: 15, + vertical: 5 + } + }, + plotOptions: { + pie: { + donut: { + size: '70%' + } + } + }, + dataLabels: { + enabled: false + }, + tooltip: { + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + stroke: { + width: 2, + colors: ['#fff'] + } + } + + const formatCurrency = (amount: number): string => { + return amount.toLocaleString('id-ID') + } + + return ( + + +
+ +
+ } + /> + + {/* Toggle Buttons */} +
+ + + + +
+ + {/* Content based on active view */} + {activeView === 'chart' ? ( + // Donut Chart View (Slide 1) +
+ +
+ ) : ( + // Table View (Slide 2) + + + + + Kategori + + Jml + + + Nilai + + + + + {productData.map((row, index) => ( + + {row.kategori} + {row.jumlah} + {formatCurrency(row.nilai)} + + ))} + + + + Total + + {totalJumlah} + + + {formatCurrency(totalNilai)} + + + +
+
+ )} + + {/* Two dot indicators at bottom */} +
+
setActiveView('chart')} + >
+
setActiveView('table')} + >
+
+
+ + ) +} + +export default PurchaseProductSalesChart diff --git a/src/views/apps/purchase/overview/overview-left/PurchaseVendorSalesChart.tsx b/src/views/apps/purchase/overview/overview-left/PurchaseVendorSalesChart.tsx new file mode 100644 index 0000000..f915817 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-left/PurchaseVendorSalesChart.tsx @@ -0,0 +1,187 @@ +'use client' + +// React Imports +import { useState } from 'react' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Button from '@mui/material/Button' +import ButtonGroup from '@mui/material/ButtonGroup' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import TableFooter from '@mui/material/TableFooter' +import Typography from '@mui/material/Typography' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Data +const customerData = [ + { pelanggan: 'Mumpuni Imam Rajata...', perusahaan: 'Fa Hassanah', nilai: 14117300 }, + { pelanggan: 'Balangga Saefullah M...', perusahaan: 'CV Siregar Prasetya Tbk', nilai: 12520640 }, + { pelanggan: 'Jais Mursita Adrians...', perusahaan: 'UD Sinaga Waluyo', nilai: 12174120 }, + { pelanggan: 'Paramita Wijayanti M...', perusahaan: 'PD Waluyo Yuniar', nilai: 11001060 }, + { pelanggan: 'Putri Puji Pertiwi S...', perusahaan: 'CV Wahyudin Tbk', nilai: 9175560 }, + { pelanggan: 'Lainnya', perusahaan: '', nilai: 63285770 } +] + +const totalNilai = customerData.reduce((sum, item) => sum + item.nilai, 0) + +// Chart series and colors +const series = customerData.map(item => item.nilai) +const colors = ['#FF6B9D', '#FFD93D', '#6BCF7F', '#4D96FF', '#B39DDB', '#E91E63'] + +const PurchaseVendorSalesChart = () => { + const [activeView, setActiveView] = useState<'chart' | 'table'>('chart') + + const options: ApexOptions = { + chart: { + type: 'donut' + }, + labels: customerData.map(item => item.pelanggan), + colors: colors, + legend: { + position: 'bottom', + horizontalAlign: 'center', + fontSize: '14px', + markers: { + width: 12, + height: 12, + radius: 2 + }, + itemMargin: { + horizontal: 15, + vertical: 5 + } + }, + plotOptions: { + pie: { + donut: { + size: '70%' + } + } + }, + dataLabels: { + enabled: false + }, + tooltip: { + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + stroke: { + width: 2, + colors: ['#fff'] + } + } + + const formatCurrency = (amount: number): string => { + return amount.toLocaleString('id-ID') + } + + return ( + + +
+ +
+ } + /> + + {/* Content based on active view */} + {activeView === 'chart' ? ( + // Donut Chart View (Slide 1) +
+ +
+ ) : ( + // Table View (Slide 2) + + + + + Vendor + + Nilai + + + + + {customerData.map((row, index) => ( + + +
+ + {row.pelanggan} + + {row.perusahaan && ( + + {row.perusahaan} + + )} +
+
+ {formatCurrency(row.nilai)} +
+ ))} +
+ + + Total + + {formatCurrency(totalNilai)} + + + +
+
+ )} + + {/* Two dot indicators at bottom */} +
+
setActiveView('chart')} + >
+
setActiveView('table')} + >
+
+
+ + ) +} + +export default PurchaseVendorSalesChart diff --git a/src/views/apps/purchase/overview/overview-left/index.tsx b/src/views/apps/purchase/overview/overview-left/index.tsx new file mode 100644 index 0000000..d2bb388 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-left/index.tsx @@ -0,0 +1,15 @@ +import PurchasePaymentRatioGaugeChart from './PurchasePaymentRatioGaugeChart' +import PurchaseProductSalesChart from './PurchaseProductChart' +import PurchaseVendorSalesChart from './PurchaseVendorSalesChart' + +const PurchaseOverviewLeft = () => { + return ( + <> + + + + + ) +} + +export default PurchaseOverviewLeft diff --git a/src/views/apps/purchase/overview/overview-right/PurchaseBillPaymentChart.tsx b/src/views/apps/purchase/overview/overview-right/PurchaseBillPaymentChart.tsx new file mode 100644 index 0000000..6c0b8ac --- /dev/null +++ b/src/views/apps/purchase/overview/overview-right/PurchaseBillPaymentChart.tsx @@ -0,0 +1,160 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const colors = { + tagihanPembelian: '#28C76F', + pembayaranPembelian: '#EA5455' +} + +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Tagihan Pembelian', + type: 'column', + data: [1250, 1180, 1350, 1450, 1200, 1520, 1380, 1150, 1650, 1400, 1290, 1700] + }, + { + name: 'Pembayaran Pembelian', + type: 'column', + data: [950, 880, 1050, 1150, 900, 1220, 1080, 850, 1350, 1100, 990, 1400] + } +] + +const PurchaseBillPaymentChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + stacked: false, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + '.000' + } + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '60%', + borderRadius: 4 + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 5, + max: 1800, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + return 'Rp ' + val + 'rb' + } + } + }, + legend: { + markers: { + width: 8, + height: 8, + offsetX: -3, + radius: 12 + }, + height: 33, + offsetY: 10, + itemMargin: { + horizontal: 10, + vertical: 0 + }, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400, + labels: { + colors: bodyColor, + useSeriesColors: false + } + }, + grid: { + borderColor, + strokeDashArray: 6 + }, + colors: [colors.tagihanPembelian, colors.pembayaranPembelian], + fill: { + opacity: 1 + } + } + + return ( + + } + /> + + + + + ) +} + +export default PurchaseBillPaymentChart diff --git a/src/views/apps/purchase/overview/overview-right/PurchaseOverviewCards.tsx b/src/views/apps/purchase/overview/overview-right/PurchaseOverviewCards.tsx new file mode 100644 index 0000000..f671737 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-right/PurchaseOverviewCards.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'PEMBELIAN', + stats: '12.932.460', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '114,7%', + subtitle: '15 Tagihan Pembelian Bulan Ini' + }, + { + title: 'PEMBAYARAN PEMBELIAN DIKIRIM', + stats: '2.573.790', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '34,0%', + subtitle: '5 Tagihan Pembelian Bulan Ini' + }, + { + title: 'MENUNGGU PEMBAYARAN PEMBELIAN', + stats: '10.358.670', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '380,6%', + subtitle: '10 Tagihan Pembelian' + }, + { + title: 'JATUH TEMPO TAGIHAN PEMBELIAN', + stats: '6.033.000', + avatarIcon: 'tabler-clock-exclamation', + avatarColor: 'error', + trend: 'neutral', + trendNumber: '100%', + subtitle: '6 Tagihan Pembelian' + } +] + +const PurchaseOverviewCards = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default PurchaseOverviewCards diff --git a/src/views/apps/purchase/overview/overview-right/PurchasePaymentReceivedChart.tsx b/src/views/apps/purchase/overview/overview-right/PurchasePaymentReceivedChart.tsx new file mode 100644 index 0000000..493ddb7 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-right/PurchasePaymentReceivedChart.tsx @@ -0,0 +1,152 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' + +// Third Party Imports +import type { ApexOptions } from 'apexcharts' + +// Components Imports +import OptionMenu from '@core/components/option-menu' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Styles Imports +import './styles.css' + +// Vars +const labelColor = 'var(--mui-palette-text-disabled)' +const bodyColor = 'var(--mui-palette-text-secondary)' +const borderColor = 'var(--mui-palette-divider)' + +const series = [ + { + name: 'Pembayaran Diterima', + data: [0, 0, 0, 0, 0, 67000000] + } +] + +const PurchasePaymentReceivedChart = () => { + const options: ApexOptions = { + chart: { + parentHeightOffset: 0, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + stroke: { + width: 3, + curve: 'smooth' + }, + markers: { + size: 6, + colors: ['#00CFE8'], + strokeColors: '#fff', + strokeWidth: 2, + hover: { + size: 8 + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val) { + return 'Rp ' + val.toLocaleString('id-ID') + } + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: ['2020', '2021', '2022', '2023', '2024', '2025'], + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: { + tickAmount: 7, + max: 70000000, + min: 0, + labels: { + style: { + colors: labelColor, + fontSize: '13px', + fontFamily: 'Public Sans', + fontWeight: 400 + }, + formatter(val: number) { + if (val === 0) return '0' + return (val / 1000000).toFixed(0) + '.000.000' + } + } + }, + legend: { + show: false + }, + grid: { + borderColor, + strokeDashArray: 6, + xaxis: { + lines: { + show: true + } + }, + yaxis: { + lines: { + show: true + } + } + }, + colors: ['#00CFE8'], + fill: { + opacity: 1 + } + } + + return ( + + +
+ + + } + /> + + + +
+ ) +} + +export default PurchasePaymentReceivedChart diff --git a/src/views/apps/purchase/overview/overview-right/index.tsx b/src/views/apps/purchase/overview/overview-right/index.tsx new file mode 100644 index 0000000..64d7b49 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-right/index.tsx @@ -0,0 +1,15 @@ +import PurchaseBillPaymentChart from './PurchaseBillPaymentChart' +import PurchaseOverviewCards from './PurchaseOverviewCards' +import PurchasePaymentReceivedChart from './PurchasePaymentReceivedChart' + +const PurchaseOverviewRight = () => { + return ( + <> + + + + + ) +} + +export default PurchaseOverviewRight diff --git a/src/views/apps/purchase/overview/overview-right/styles.css b/src/views/apps/purchase/overview/overview-right/styles.css new file mode 100644 index 0000000..a93db94 --- /dev/null +++ b/src/views/apps/purchase/overview/overview-right/styles.css @@ -0,0 +1,15 @@ +#tagihan-pembayaran-pembelian .apexcharts-legend .apexcharts-legend-series { + border: 1px solid var(--mui-palette-divider); + border-radius: var(--mui-shape-borderRadius); + block-size: 83%; + padding-block: 4px; + padding-inline: 16px; +} + +#pembayaran-diterima .apexcharts-legend .apexcharts-legend-series { + border: 1px solid var(--mui-palette-divider); + border-radius: var(--mui-shape-borderRadius); + block-size: 83%; + padding-block: 4px; + padding-inline: 16px; +} From dfa82180174336bb1aebbade641ed6c382d0590e Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 17:42:49 +0700 Subject: [PATCH 08/42] feat: purchase order page --- .../apps/purchase/purchase-orders/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 3 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/purchase-order.ts | 224 +++++++++ src/types/apps/purchaseOrderTypes.ts | 11 + .../list/PurchaseOrderListTable.tsx | 452 ++++++++++++++++++ .../purchase/purchase-orders/list/index.tsx | 19 + 8 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/page.tsx create mode 100644 src/data/dummy/purchase-order.ts create mode 100644 src/types/apps/purchaseOrderTypes.ts create mode 100644 src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx create mode 100644 src/views/apps/purchase/purchase-orders/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/page.tsx new file mode 100644 index 0000000..2e2bcba --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/page.tsx @@ -0,0 +1,7 @@ +import PurchaseOrderList from '@/views/apps/purchase/purchase-orders/list' + +const PurchaseOrdersPage = () => { + return +} + +export default PurchaseOrdersPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index eee95af..e1fe92b 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -97,6 +97,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].overview} + + {dictionary['navigation'].purchase_orders} + }> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index c20fde3..f153a38 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -114,6 +114,7 @@ "dailyReport": "Daily Report", "vendor": "Vendor", "sales": "Sales", - "purchase_text": "Purchase" + "purchase_text": "Purchase", + "purchase_orders": "Purchase Orders" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 7c4c7fe..9ad2d0c 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -114,6 +114,7 @@ "dailyReport": "Laporan Harian", "vendor": "Vendor", "sales": "Penjualan", - "purchase_text": "Pembelian" + "purchase_text": "Pembelian", + "purchase_orders": "Pesanan Pembelian" } } diff --git a/src/data/dummy/purchase-order.ts b/src/data/dummy/purchase-order.ts new file mode 100644 index 0000000..6ac722a --- /dev/null +++ b/src/data/dummy/purchase-order.ts @@ -0,0 +1,224 @@ +import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes' + +export const purchaseOrdersData: PurchaseOrderType[] = [ + { + id: 1, + number: 'PO/00040', + vendorName: 'Kairav Wijaya Nababan', + vendorCompany: 'Yayasan Haryanto Tbk', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Disetujui', + total: 2037170 + }, + { + id: 2, + number: 'PO/00041', + vendorName: 'Sari Melani Hutapea', + vendorCompany: 'PT Santoso Jaya', + reference: 'REF-2025-001', + date: '03/09/2025', + dueDate: '17/09/2025', + status: 'Draft', + total: 1875500 + }, + { + id: 3, + number: 'PO/00042', + vendorName: 'Budi Prasetyo Manullang', + vendorCompany: 'CV Wijaya Makmur', + reference: 'REF-2025-002', + date: '07/09/2025', + dueDate: '21/09/2025', + status: 'Dikirim Sebagian', + total: 3250750 + }, + { + id: 4, + number: 'PO/00043', + vendorName: 'Maya Sari Lumban Gaol', + vendorCompany: 'PT Indah Permata', + reference: '', + date: '02/09/2025', + dueDate: '16/09/2025', + status: 'Selesai', + total: 980250 + }, + { + id: 5, + number: 'PO/00044', + vendorName: 'Rahman Hakim Siahaan', + vendorCompany: 'Toko Bahagia Sejahtera', + reference: 'REF-2025-003', + date: '08/09/2025', + dueDate: '22/09/2025', + status: 'Draft', + total: 4125890 + }, + { + id: 6, + number: 'PO/00045', + vendorName: 'Dewi Anggraini Panjaitan', + vendorCompany: 'PT Maju Bersama', + reference: 'REF-2025-004', + date: '01/09/2025', + dueDate: '15/09/2025', + status: 'Selesai', + total: 2678300 + }, + { + id: 7, + number: 'PO/00046', + vendorName: 'Agung Wijaya Simbolon', + vendorCompany: 'CV Karya Mandiri', + reference: '', + date: '06/09/2025', + dueDate: '20/09/2025', + status: 'Disetujui', + total: 1456780 + }, + { + id: 8, + number: 'PO/00047', + vendorName: 'Fitri Handayani Sitorus', + vendorCompany: 'PT Global Nusantara', + reference: 'REF-2025-005', + date: '04/09/2025', + dueDate: '18/09/2025', + status: 'Dikirim Sebagian', + total: 5892450 + }, + { + id: 9, + number: 'PO/00048', + vendorName: 'Andi Setiawan Tampubolon', + vendorCompany: 'Yayasan Pembangunan Tbk', + reference: 'REF-2025-006', + date: '09/09/2025', + dueDate: '23/09/2025', + status: 'Draft', + total: 3567120 + }, + { + id: 10, + number: 'PO/00049', + vendorName: 'Rina Maharani Hutasoit', + vendorCompany: 'PT Sejahtera Abadi', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Disetujui', + total: 2234680 + }, + { + id: 11, + number: 'PO/00050', + vendorName: 'Joko Santoso Nainggolan', + vendorCompany: 'CV Berkah Jaya', + reference: 'REF-2025-007', + date: '03/09/2025', + dueDate: '17/09/2025', + status: 'Selesai', + total: 1789560 + }, + { + id: 12, + number: 'PO/00051', + vendorName: 'Linda Safitri Simanjuntak', + vendorCompany: 'PT Harapan Bangsa', + reference: 'REF-2025-008', + date: '07/09/2025', + dueDate: '21/09/2025', + status: 'Dikirim Sebagian', + total: 4321870 + }, + { + id: 13, + number: 'PO/00052', + vendorName: 'Irfan Maulana Pasaribu', + vendorCompany: 'CV Sumber Rejeki', + reference: 'REF-2025-009', + date: '10/09/2025', + dueDate: '24/09/2025', + status: 'Draft', + total: 2567890 + }, + { + id: 14, + number: 'PO/00053', + vendorName: 'Siska Permata Sinaga', + vendorCompany: 'PT Mitra Sukses', + reference: '', + date: '11/09/2025', + dueDate: '25/09/2025', + status: 'Disetujui', + total: 3456780 + }, + { + id: 15, + number: 'PO/00054', + vendorName: 'Rizky Aditya Siregar', + vendorCompany: 'Toko Aman Sentosa', + reference: 'REF-2025-010', + date: '08/09/2025', + dueDate: '22/09/2025', + status: 'Selesai', + total: 1234567 + }, + { + id: 16, + number: 'PO/00055', + vendorName: 'Nina Sari Hutabarat', + vendorCompany: 'PT Cahaya Terang', + reference: 'REF-2025-011', + date: '12/09/2025', + dueDate: '26/09/2025', + status: 'Dikirim Sebagian', + total: 5678900 + }, + { + id: 17, + number: 'PO/00056', + vendorName: 'Doni Prasetya Situmorang', + vendorCompany: 'CV Barokah Makmur', + reference: '', + date: '09/09/2025', + dueDate: '23/09/2025', + status: 'Draft', + total: 987654 + }, + { + id: 18, + number: 'PO/00057', + vendorName: 'Anita Dewi Marpaung', + vendorCompany: 'Yayasan Karya Bhakti', + reference: 'REF-2025-012', + date: '06/09/2025', + dueDate: '20/09/2025', + status: 'Disetujui', + total: 4567123 + }, + { + id: 19, + number: 'PO/00058', + vendorName: 'Tommy Wijaya Samosir', + vendorCompany: 'PT Bintang Timur', + reference: 'REF-2025-013', + date: '13/09/2025', + dueDate: '27/09/2025', + status: 'Selesai', + total: 2345678 + }, + { + id: 20, + number: 'PO/00059', + vendorName: 'Lestari Indah Pakpahan', + vendorCompany: 'CV Harmoni Jaya', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Dikirim Sebagian', + total: 3789012 + } +] diff --git a/src/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts new file mode 100644 index 0000000..c38cae7 --- /dev/null +++ b/src/types/apps/purchaseOrderTypes.ts @@ -0,0 +1,11 @@ +export type PurchaseOrderType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + dueDate: string + status: string + total: number +} diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx new file mode 100644 index 0000000..f05c540 --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -0,0 +1,452 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes' +import { purchaseOrdersData } from '@/data/dummy/purchase-order' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type PurchaseOrderTypeWithAction = PurchaseOrderType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping +const getStatusColor = (status: string) => { + switch (status) { + case 'Draft': + return 'secondary' + case 'Disetujui': + return 'primary' + case 'Dikirim Sebagian': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const PurchaseOrderListTable = () => { + const dispatch = useDispatch() + + // States + const [addPOOpen, setAddPOOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [poId, setPOId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(purchaseOrdersData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = purchaseOrdersData + + // Filter by search + if (search) { + filtered = filtered.filter( + po => + po.number.toLowerCase().includes(search.toLowerCase()) || + po.vendorName.toLowerCase().includes(search.toLowerCase()) || + po.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + po.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(po => po.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handlePOClick = (poId: string) => { + console.log('Navigasi ke detail PO:', poId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor PO', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Vendor', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as PurchaseOrderType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Draft', 'Disetujui', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Purchase Order' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default PurchaseOrderListTable diff --git a/src/views/apps/purchase/purchase-orders/list/index.tsx b/src/views/apps/purchase/purchase-orders/list/index.tsx new file mode 100644 index 0000000..29fa55b --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import PurchaseOrderListTable from './PurchaseOrderListTable' + +const PurchaseOrderList = () => { + return ( + + + + + + ) +} + +export default PurchaseOrderList From 4c4579f00967d568b5f05d11cae4118898e170e2 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 22:47:05 +0700 Subject: [PATCH 09/42] feat: Purchase Order Add --- .../purchase/purchase-orders/add/page.tsx | 19 + src/components/ImageUpload.tsx | 291 +++++++++ src/types/apps/purchaseOrderTypes.ts | 55 ++ .../add/PurchaseOrderAddForm.tsx | 121 ++++ .../add/PurchaseOrderAddHeader.tsx | 19 + .../add/PurchaseOrderBasicInfo.tsx | 197 ++++++ .../add/PurchaseOrderIngredientsTable.tsx | 249 ++++++++ .../add/PurchaseOrderSummary.tsx | 578 ++++++++++++++++++ .../list/PurchaseOrderListTable.tsx | 8 +- 9 files changed, 1534 insertions(+), 3 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx create mode 100644 src/components/ImageUpload.tsx create mode 100644 src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx create mode 100644 src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx create mode 100644 src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx create mode 100644 src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx create mode 100644 src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx new file mode 100644 index 0000000..71c0036 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx @@ -0,0 +1,19 @@ +import Grid from '@mui/material/Grid2' + +import PurchaseOrderAddHeader from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader' +import PurchaseOrderAddForm from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm' + +const PurchaseOrderAddPage = () => { + return ( + + + + + + + + + ) +} + +export default PurchaseOrderAddPage diff --git a/src/components/ImageUpload.tsx b/src/components/ImageUpload.tsx new file mode 100644 index 0000000..464785d --- /dev/null +++ b/src/components/ImageUpload.tsx @@ -0,0 +1,291 @@ +'use client' + +// React Imports +import { useEffect, useState } from 'react' + +// MUI Imports +import type { BoxProps } from '@mui/material/Box' +import Button from '@mui/material/Button' +import IconButton from '@mui/material/IconButton' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import Typography from '@mui/material/Typography' +import { styled } from '@mui/material/styles' + +// Third-party Imports +import { useDropzone } from 'react-dropzone' + +// Component Imports +import Link from '@components/Link' +import CustomAvatar from '@core/components/mui/Avatar' + +// Styled Component Imports +import AppReactDropzone from '@/libs/styles/AppReactDropzone' + +type FileProp = { + name: string + type: string + size: number +} + +interface ImageUploadProps { + // Required props + onUpload: (file: File) => Promise | string // Returns image URL + + // Optional customization props + title?: string | null // Made nullable + currentImageUrl?: string + onImageRemove?: () => void + onImageChange?: (url: string) => void + + // Upload state + isUploading?: boolean + + // UI customization + maxFileSize?: number // in bytes + acceptedFileTypes?: string[] + showUrlOption?: boolean + uploadButtonText?: string + browseButtonText?: string + dragDropText?: string + replaceText?: string + + // Style customization + className?: string + disabled?: boolean +} + +// Styled Dropzone Component +const Dropzone = styled(AppReactDropzone)(({ theme }) => ({ + '& .dropzone': { + minHeight: 'unset', + padding: theme.spacing(12), + [theme.breakpoints.down('sm')]: { + paddingInline: theme.spacing(5) + }, + '&+.MuiList-root .MuiListItem-root .file-name': { + fontWeight: theme.typography.body1.fontWeight + } + } +})) + +const ImageUpload: React.FC = ({ + onUpload, + title = null, // Default to null + currentImageUrl = '', + onImageRemove, + onImageChange, + isUploading = false, + maxFileSize = 5 * 1024 * 1024, // 5MB default + acceptedFileTypes = ['image/*'], + showUrlOption = true, + uploadButtonText = 'Upload', + browseButtonText = 'Browse Image', + dragDropText = 'Drag and Drop Your Image Here.', + replaceText = 'Drop New Image to Replace', + className = '', + disabled = false +}) => { + // States + const [files, setFiles] = useState([]) + const [error, setError] = useState('') + + const handleUpload = async () => { + if (!files.length) return + + try { + setError('') + const imageUrl = await onUpload(files[0]) + + if (typeof imageUrl === 'string') { + onImageChange?.(imageUrl) + setFiles([]) // Clear files after successful upload + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed') + } + } + + // Hooks + const { getRootProps, getInputProps } = useDropzone({ + onDrop: (acceptedFiles: File[]) => { + setError('') + + if (acceptedFiles.length === 0) return + + const file = acceptedFiles[0] + + // Validate file size + if (file.size > maxFileSize) { + setError(`File size should be less than ${formatFileSize(maxFileSize)}`) + return + } + + // Replace files instead of adding to them + setFiles([file]) + }, + accept: acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}), + disabled: disabled || isUploading + }) + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + const renderFilePreview = (file: FileProp) => { + if (file.type.startsWith('image')) { + return {file.name} + } else { + return + } + } + + const handleRemoveFile = (file: FileProp) => { + const filtered = files.filter((i: FileProp) => i.name !== file.name) + setFiles(filtered) + setError('') + } + + const handleRemoveCurrentImage = () => { + onImageRemove?.() + } + + const handleRemoveAllFiles = () => { + setFiles([]) + setError('') + } + + const fileList = files.map((file: FileProp) => ( + +
+
{renderFilePreview(file)}
+
+ + {file.name} + + + {formatFileSize(file.size)} + +
+
+ handleRemoveFile(file)} disabled={isUploading}> + + +
+ )) + + return ( + + {/* Conditional title and URL option header */} + {title && ( +
+ + {title} + + {showUrlOption && ( + + Add media from URL + + )} +
+ )} + +
+ +
+ + + + {currentImageUrl && !files.length ? replaceText : dragDropText} + or + +
+
+ + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Show current image if it exists */} + {currentImageUrl && !files.length && ( +
+ + Current Image: + +
+
+ Current image +
+ + Current image + + + Uploaded image + +
+
+ {onImageRemove && ( + + + + )} +
+
+ )} + + {/* File list and upload buttons */} + {files.length > 0 && ( + <> + {fileList} +
+ + +
+ + )} +
+ ) +} + +export default ImageUpload + +// ===== USAGE EXAMPLES ===== + +// 1. Without title +// setImageUrl('')} +// /> + +// 2. With title +// setImageUrl('')} +// /> + +// 3. Explicitly set title to null +// setImageUrl('')} +// /> diff --git a/src/types/apps/purchaseOrderTypes.ts b/src/types/apps/purchaseOrderTypes.ts index c38cae7..987560f 100644 --- a/src/types/apps/purchaseOrderTypes.ts +++ b/src/types/apps/purchaseOrderTypes.ts @@ -9,3 +9,58 @@ export type PurchaseOrderType = { status: string total: number } + +export interface IngredientItem { + id: number + ingredient: { label: string; value: string } | null + deskripsi: string + kuantitas: number + satuan: { label: string; value: string } | null + discount: string + harga: number + pajak: { label: string; value: string } | null + waste: { label: string; value: string } | null + total: number +} + +export interface PurchaseOrderFormData { + vendor: { label: string; value: string } | null + nomor: string + tglTransaksi: string + tglJatuhTempo: string + referensi: string + termin: { label: string; value: string } | null + hargaTermasukPajak: boolean + showShippingInfo: boolean + tanggalPengiriman: string + ekspedisi: { label: string; value: string } | null + noResi: string + showPesan: boolean + showAttachment: boolean + showTambahDiskon: boolean + showBiayaPengiriman: boolean + showBiayaTransaksi: boolean + showUangMuka: boolean + pesan: string + ingredientItems: IngredientItem[] + transactionCosts?: TransactionCost[] + subtotal?: number + discountType?: 'percentage' | 'fixed' + downPaymentType?: 'percentage' | 'fixed' + discountValue?: string + shippingCost?: string + transactionCost?: string + downPayment?: string +} + +export interface TransactionCost { + id: string + type: string + name: string + amount: string +} + +export interface DropdownOption { + label: string + value: string +} diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx new file mode 100644 index 0000000..77814ef --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx @@ -0,0 +1,121 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' +import PurchaseOrderBasicInfo from './PurchaseOrderBasicInfo' +import PurchaseOrderIngredientsTable from './PurchaseOrderIngredientsTable' +import PurchaseOrderSummary from './PurchaseOrderSummary' + +const PurchaseOrderAddForm: React.FC = () => { + const [formData, setFormData] = useState({ + vendor: null, + nomor: 'PO/00043', + tglTransaksi: '2025-09-09', + tglJatuhTempo: '2025-09-10', + referensi: '', + termin: null, + hargaTermasukPajak: true, + // Shipping info + showShippingInfo: false, + tanggalPengiriman: '', + ekspedisi: null, + noResi: '', + // Bottom section toggles + showPesan: false, + showAttachment: false, + showTambahDiskon: false, + showBiayaPengiriman: false, + showBiayaTransaksi: false, + showUangMuka: false, + pesan: '', + // Ingredient items (updated from productItems) + ingredientItems: [ + { + id: 1, + ingredient: null, + deskripsi: '', + kuantitas: 1, + satuan: null, + discount: '0', + harga: 0, + pajak: null, + waste: null, + total: 0 + } + ] + }) + + const handleInputChange = (field: keyof PurchaseOrderFormData, value: any): void => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + const handleIngredientChange = (index: number, field: keyof IngredientItem, value: any): void => { + setFormData(prev => { + const newItems = [...prev.ingredientItems] + newItems[index] = { ...newItems[index], [field]: value } + + // Auto-calculate total if price or quantity changes + if (field === 'harga' || field === 'kuantitas') { + const item = newItems[index] + item.total = item.harga * item.kuantitas + } + + return { ...prev, ingredientItems: newItems } + }) + } + + const addIngredientItem = (): void => { + const newItem: IngredientItem = { + id: Date.now(), + ingredient: null, + deskripsi: '', + kuantitas: 1, + satuan: null, + discount: '0%', + harga: 0, + pajak: null, + waste: null, + total: 0 + } + setFormData(prev => ({ + ...prev, + ingredientItems: [...prev.ingredientItems, newItem] + })) + } + + const removeIngredientItem = (index: number): void => { + setFormData(prev => ({ + ...prev, + ingredientItems: prev.ingredientItems.filter((_, i) => i !== index) + })) + } + + return ( + + + + {/* Basic Info Section */} + + + {/* Ingredients Table Section */} + + + {/* Summary Section */} + + + + + ) +} + +export default PurchaseOrderAddForm diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx new file mode 100644 index 0000000..f06706a --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const PurchaseOrderAddHeader = () => { + return ( +
+
+ + Tambah Pesanan Pembelian + +
+
+ ) +} + +export default PurchaseOrderAddHeader diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx new file mode 100644 index 0000000..6d8adcf --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx @@ -0,0 +1,197 @@ +'use client' + +import React from 'react' +import { Button, Switch, FormControlLabel } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import CustomTextField from '@/@core/components/mui/TextField' +import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' + +interface PurchaseOrderBasicInfoProps { + formData: PurchaseOrderFormData + handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void +} + +const PurchaseOrderBasicInfo: React.FC = ({ formData, handleInputChange }) => { + // Sample data for dropdowns + const vendorOptions: DropdownOption[] = [ + { label: 'Vendor A', value: 'vendor_a' }, + { label: 'Vendor B', value: 'vendor_b' }, + { label: 'Vendor C', value: 'vendor_c' } + ] + + const terminOptions: DropdownOption[] = [ + { label: 'Net 30', value: 'net_30' }, + { label: 'Net 15', value: 'net_15' }, + { label: 'Net 60', value: 'net_60' }, + { label: 'Cash on Delivery', value: 'cod' } + ] + + const ekspedisiOptions: DropdownOption[] = [ + { label: 'JNE', value: 'jne' }, + { label: 'J&T Express', value: 'jnt' }, + { label: 'SiCepat', value: 'sicepat' }, + { label: 'Pos Indonesia', value: 'pos' }, + { label: 'TIKI', value: 'tiki' } + ] + + return ( + <> + {/* Row 1 - Vendor dan Nomor */} + + handleInputChange('vendor', newValue)} + renderInput={params => } + /> + + + ) => handleInputChange('nomor', e.target.value)} + InputProps={{ + readOnly: true + }} + /> + + + {/* Row 2 - Tgl. Transaksi, Tgl. Jatuh Tempo, Termin */} + + ) => handleInputChange('tglTransaksi', e.target.value)} + InputLabelProps={{ + shrink: true + }} + /> + + + ) => handleInputChange('tglJatuhTempo', e.target.value)} + InputLabelProps={{ + shrink: true + }} + /> + + + handleInputChange('termin', newValue)} + renderInput={params => } + /> + + + {/* Row 3 - Tampilkan Informasi Pengiriman */} + + + + + {/* Shipping Information - Conditional */} + {formData.showShippingInfo && ( + <> + + ) => + handleInputChange('tanggalPengiriman', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + /> + + + handleInputChange('ekspedisi', newValue)} + renderInput={params => ( + + )} + /> + + + ) => handleInputChange('noResi', e.target.value)} + /> + + + )} + + {/* Row 4 - Referensi, SKU, Switch Pajak */} + + ) => handleInputChange('referensi', e.target.value)} + /> + + + + + + ) => + handleInputChange('hargaTermasukPajak', e.target.checked) + } + color='primary' + /> + } + label='Harga termasuk pajak' + sx={{ + marginLeft: 0, + '& .MuiFormControlLabel-label': { + fontSize: '14px', + color: 'text.secondary' + } + }} + /> + + + ) +} + +export default PurchaseOrderBasicInfo diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx new file mode 100644 index 0000000..8cb8836 --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx @@ -0,0 +1,249 @@ +'use client' + +import React from 'react' +import { Button, Typography } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import CustomTextField from '@/@core/components/mui/TextField' +import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' + +interface PurchaseOrderIngredientsTableProps { + formData: PurchaseOrderFormData + handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void + addIngredientItem: () => void + removeIngredientItem: (index: number) => void +} + +const PurchaseOrderIngredientsTable: React.FC = ({ + formData, + handleIngredientChange, + addIngredientItem, + removeIngredientItem +}) => { + const ingredientOptions = [ + { label: 'Tepung Terigu Premium', value: 'tepung_terigu_premium' }, + { label: 'Gula Pasir Halus', value: 'gula_pasir_halus' }, + { label: 'Mentega Unsalted', value: 'mentega_unsalted' }, + { label: 'Telur Ayam Grade A', value: 'telur_ayam_grade_a' }, + { label: 'Vanilla Extract', value: 'vanilla_extract' }, + { label: 'Coklat Chips', value: 'coklat_chips' } + ] + + const satuanOptions = [ + { label: 'KG', value: 'kg' }, + { label: 'GRAM', value: 'gram' }, + { label: 'LITER', value: 'liter' }, + { label: 'ML', value: 'ml' }, + { label: 'PCS', value: 'pcs' }, + { label: 'PACK', value: 'pack' } + ] + + const pajakOptions = [ + { label: 'PPN 11%', value: 'ppn_11' }, + { label: 'PPN 0%', value: 'ppn_0' }, + { label: 'Bebas Pajak', value: 'tax_free' } + ] + + const wasteOptions = [ + { label: '2%', value: '2' }, + { label: '5%', value: '5' }, + { label: '10%', value: '10' }, + { label: '15%', value: '15' }, + { label: 'Custom', value: 'custom' } + ] + + return ( + + + Bahan Baku / Ingredients + + + {/* Table Header */} + + + + Bahan Baku + + + + + Deskripsi + + + + + Kuantitas + + + + + Satuan + + + + + Discount + + + + + Harga + + + + + Pajak + + + + + Waste + + + + + Total + + + + + + {/* Ingredient Items */} + {formData.ingredientItems.map((item: IngredientItem, index: number) => ( + + + handleIngredientChange(index, 'ingredient', newValue)} + renderInput={params => ( + + )} + /> + + + ) => + handleIngredientChange(index, 'deskripsi', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleIngredientChange(index, 'satuan', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'discount', e.target.value) + } + placeholder='0%' + /> + + + ) => { + const value = e.target.value + + if (value === '') { + // Jika kosong, set ke null atau undefined, bukan 0 + handleIngredientChange(index, 'harga', null) // atau undefined + return + } + + const numericValue = parseFloat(value) + handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) + }} + inputProps={{ min: 0, step: 'any' }} + placeholder='0' + /> + + + handleIngredientChange(index, 'pajak', newValue)} + renderInput={params => } + /> + + + handleIngredientChange(index, 'waste', newValue)} + renderInput={params => } + /> + + + + + + + + + ))} + + {/* Add New Item Button */} + + + + + + + ) +} + +export default PurchaseOrderIngredientsTable diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx new file mode 100644 index 0000000..2c2a51d --- /dev/null +++ b/src/views/apps/purchase/purchase-orders/add/PurchaseOrderSummary.tsx @@ -0,0 +1,578 @@ +'use client' + +import React from 'react' +import { Button, Typography, Box, ToggleButton, ToggleButtonGroup, InputAdornment, IconButton } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomTextField from '@/@core/components/mui/TextField' +import { PurchaseOrderFormData, TransactionCost } from '@/types/apps/purchaseOrderTypes' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import ImageUpload from '@/components/ImageUpload' + +interface PurchaseOrderSummaryProps { + formData: PurchaseOrderFormData + handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void +} + +const PurchaseOrderSummary: React.FC = ({ formData, handleInputChange }) => { + // Initialize transaction costs if not exist + const transactionCosts = formData.transactionCosts || [] + + // Options for transaction cost types + const transactionCostOptions = [ + { label: 'Biaya Admin', value: 'admin' }, + { label: 'Pajak', value: 'pajak' }, + { label: 'Materai', value: 'materai' }, + { label: 'Lainnya', value: 'lainnya' } + ] + + // Add new transaction cost + const addTransactionCost = () => { + const newCost: TransactionCost = { + id: Date.now().toString(), + type: '', + name: '', + amount: '' + } + handleInputChange('transactionCosts', [...transactionCosts, newCost]) + } + + // Remove transaction cost + const removeTransactionCost = (id: string) => { + const filtered = transactionCosts.filter((cost: TransactionCost) => cost.id !== id) + handleInputChange('transactionCosts', filtered) + } + + // Update transaction cost + const updateTransactionCost = (id: string, field: keyof TransactionCost, value: string) => { + const updated = transactionCosts.map((cost: TransactionCost) => + cost.id === id ? { ...cost, [field]: value } : cost + ) + handleInputChange('transactionCosts', updated) + } + + // Calculate discount amount based on percentage or fixed amount + const calculateDiscount = () => { + if (!formData.discountValue) return 0 + + const subtotal = formData.subtotal || 0 + if (formData.discountType === 'percentage') { + return (subtotal * parseFloat(formData.discountValue)) / 100 + } + return parseFloat(formData.discountValue) + } + + const discountAmount = calculateDiscount() + const shippingCost = parseFloat(formData.shippingCost || '0') + + // Calculate total transaction costs + const totalTransactionCost = transactionCosts.reduce((sum: number, cost: TransactionCost) => { + return sum + parseFloat(cost.amount || '0') + }, 0) + + const downPayment = parseFloat(formData.downPayment || '0') + + // Calculate total (subtotal - discount + shipping + transaction costs) + const total = (formData.subtotal || 0) - discountAmount + shippingCost + totalTransactionCost + + // Calculate remaining balance (total - down payment) + const remainingBalance = total - downPayment + + const handleUpload = async (file: File): Promise => { + // Simulate upload + return new Promise(resolve => { + setTimeout(() => { + resolve(URL.createObjectURL(file)) + }, 1000) + }) + } + + return ( + + + {/* Left Side - Pesan and Attachment */} + + {/* Pesan Section */} + + + {formData.showPesan && ( + + ) => handleInputChange('pesan', e.target.value)} + /> + + )} + + + {/* Attachment Section */} + + + {formData.showAttachment && ( + + )} + + + + {/* Right Side - Totals */} + + + {/* Sub Total */} + + + Sub Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(formData.subtotal || 0)} + + + + {/* Additional Options */} + + {/* Tambah Diskon */} + + + + {/* Show input form when showTambahDiskon is true */} + {formData.showTambahDiskon && ( + + + ) => + handleInputChange('discountValue', e.target.value) + } + sx={{ flex: 1 }} + InputProps={{ + endAdornment: + formData.discountType === 'percentage' ? ( + % + ) : undefined + }} + /> + { + if (newValue) handleInputChange('discountType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + )} + + + {/* Biaya Pengiriman */} + + + + {/* Show input form when showBiayaPengiriman is true */} + {formData.showBiayaPengiriman && ( + + + Biaya pengiriman + + ) => + handleInputChange('shippingCost', e.target.value) + } + sx={{ flex: 1 }} + InputProps={{ + startAdornment: Rp + }} + /> + + )} + + + {/* Biaya Transaksi - Multiple */} + + + + {/* Show multiple transaction cost inputs */} + {formData.showBiayaTransaksi && ( + + {transactionCosts.map((cost: TransactionCost, index: number) => ( + + {/* Remove button */} + removeTransactionCost(cost.id)} + sx={{ + color: 'error.main', + border: '1px solid', + borderColor: 'error.main', + borderRadius: '50%', + width: 28, + height: 28, + '&:hover': { + backgroundColor: 'error.lighter' + } + }} + > + + + + {/* Type AutoComplete */} + (typeof option === 'string' ? option : option.label)} + value={transactionCostOptions.find(option => option.value === cost.type) || null} + onChange={(_, newValue) => { + updateTransactionCost(cost.id, 'type', newValue ? newValue.value : '') + }} + renderInput={params => ( + + )} + sx={{ minWidth: 180 }} + noOptionsText='Tidak ada pilihan' + /> + + {/* Name input */} + ) => + updateTransactionCost(cost.id, 'name', e.target.value) + } + sx={{ flex: 1 }} + /> + + {/* Amount input */} + ) => + updateTransactionCost(cost.id, 'amount', e.target.value) + } + sx={{ width: 120 }} + InputProps={{ + startAdornment: Rp + }} + /> + + ))} + + {/* Add more button */} + + + )} + + + + {/* Total */} + + + Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(total)} + + + + {/* Uang Muka */} + + + + {/* Dropdown */} + (typeof option === 'string' ? option : option.label)} + value={{ label: '1-10003 Gi...', value: '1-10003' }} + onChange={(_, newValue) => { + // Handle change if needed + }} + renderInput={params => } + sx={{ minWidth: 120 }} + /> + + {/* Amount input */} + ) => + handleInputChange('downPayment', e.target.value) + } + sx={{ width: '80px' }} + inputProps={{ + style: { textAlign: 'center' } + }} + /> + + {/* Percentage/Fixed toggle */} + { + if (newValue) handleInputChange('downPaymentType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + {/* Right side text */} + + Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} + + + + + {/* Sisa Tagihan */} + + + Sisa Tagihan + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(remainingBalance)} + + + + {/* Save Button */} + + + + + + ) +} + +export default PurchaseOrderSummary diff --git a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx index f05c540..33cbab8 100644 --- a/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx +++ b/src/views/apps/purchase/purchase-orders/list/PurchaseOrderListTable.tsx @@ -41,6 +41,7 @@ import TablePaginationComponent from '@/components/TablePaginationComponent' import Loading from '@/components/layout/shared/Loading' import { PurchaseOrderType } from '@/types/apps/purchaseOrderTypes' import { purchaseOrdersData } from '@/data/dummy/purchase-order' +import { getLocalizedUrl } from '@/utils/i18n' declare module '@tanstack/table-core' { interface FilterFns { @@ -367,11 +368,12 @@ const PurchaseOrderListTable = () => { From 49ed3386beabefefd37c9bbd3f0c143cc9ed7639 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 23:05:23 +0700 Subject: [PATCH 10/42] feat: purchase form --- .../apps/purchase/purchase-orders/add/page.tsx | 4 ++-- .../PurchaseAddForm.tsx} | 16 ++++++++-------- .../PurchaseBasicInfo.tsx} | 6 +++--- .../PurchaseIngredientsTable.tsx} | 6 +++--- .../PurchaseSummary.tsx} | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) rename src/views/apps/purchase/{purchase-orders/add/PurchaseOrderAddForm.tsx => purchase-form/PurchaseAddForm.tsx} (84%) rename src/views/apps/purchase/{purchase-orders/add/PurchaseOrderBasicInfo.tsx => purchase-form/PurchaseBasicInfo.tsx} (97%) rename src/views/apps/purchase/{purchase-orders/add/PurchaseOrderIngredientsTable.tsx => purchase-form/PurchaseIngredientsTable.tsx} (97%) rename src/views/apps/purchase/{purchase-orders/add/PurchaseOrderSummary.tsx => purchase-form/PurchaseSummary.tsx} (99%) diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx index 71c0036..52cb4b2 100644 --- a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-orders/add/page.tsx @@ -1,7 +1,7 @@ import Grid from '@mui/material/Grid2' import PurchaseOrderAddHeader from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddHeader' -import PurchaseOrderAddForm from '@/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm' +import PurchaseAddForm from '@/views/apps/purchase/purchase-form/PurchaseAddForm' const PurchaseOrderAddPage = () => { return ( @@ -10,7 +10,7 @@ const PurchaseOrderAddPage = () => {
- +
) diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx similarity index 84% rename from src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx rename to src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx index 77814ef..7a158d9 100644 --- a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderAddForm.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseAddForm.tsx @@ -4,11 +4,11 @@ import React, { useState } from 'react' import { Card, CardContent } from '@mui/material' import Grid from '@mui/material/Grid2' import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' -import PurchaseOrderBasicInfo from './PurchaseOrderBasicInfo' -import PurchaseOrderIngredientsTable from './PurchaseOrderIngredientsTable' -import PurchaseOrderSummary from './PurchaseOrderSummary' +import PurchaseBasicInfo from './PurchaseBasicInfo' +import PurchaseIngredientsTable from './PurchaseIngredientsTable' +import PurchaseSummary from './PurchaseSummary' -const PurchaseOrderAddForm: React.FC = () => { +const PurchaseAddForm: React.FC = () => { const [formData, setFormData] = useState({ vendor: null, nomor: 'PO/00043', @@ -100,10 +100,10 @@ const PurchaseOrderAddForm: React.FC = () => { {/* Basic Info Section */} - + {/* Ingredients Table Section */} - { /> {/* Summary Section */} - + ) } -export default PurchaseOrderAddForm +export default PurchaseAddForm diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx b/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx similarity index 97% rename from src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx rename to src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx index 6d8adcf..6cce349 100644 --- a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderBasicInfo.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseBasicInfo.tsx @@ -7,12 +7,12 @@ import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' import { DropdownOption, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' -interface PurchaseOrderBasicInfoProps { +interface PurchaseBasicInfoProps { formData: PurchaseOrderFormData handleInputChange: (field: keyof PurchaseOrderFormData, value: any) => void } -const PurchaseOrderBasicInfo: React.FC = ({ formData, handleInputChange }) => { +const PurchaseBasicInfo: React.FC = ({ formData, handleInputChange }) => { // Sample data for dropdowns const vendorOptions: DropdownOption[] = [ { label: 'Vendor A', value: 'vendor_a' }, @@ -194,4 +194,4 @@ const PurchaseOrderBasicInfo: React.FC = ({ formDat ) } -export default PurchaseOrderBasicInfo +export default PurchaseBasicInfo diff --git a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx similarity index 97% rename from src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx rename to src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx index 8cb8836..d319568 100644 --- a/src/views/apps/purchase/purchase-orders/add/PurchaseOrderIngredientsTable.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx @@ -7,14 +7,14 @@ import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' import { IngredientItem, PurchaseOrderFormData } from '@/types/apps/purchaseOrderTypes' -interface PurchaseOrderIngredientsTableProps { +interface PurchaseIngredientsTableProps { formData: PurchaseOrderFormData handleIngredientChange: (index: number, field: keyof IngredientItem, value: any) => void addIngredientItem: () => void removeIngredientItem: (index: number) => void } -const PurchaseOrderIngredientsTable: React.FC = ({ +const PurchaseIngredientsTable: React.FC = ({ formData, handleIngredientChange, addIngredientItem, @@ -246,4 +246,4 @@ const PurchaseOrderIngredientsTable: React.FC void } -const PurchaseOrderSummary: React.FC = ({ formData, handleInputChange }) => { +const PurchaseSummary: React.FC = ({ formData, handleInputChange }) => { // Initialize transaction costs if not exist const transactionCosts = formData.transactionCosts || [] @@ -575,4 +575,4 @@ const PurchaseOrderSummary: React.FC = ({ formData, h ) } -export default PurchaseOrderSummary +export default PurchaseSummary From b20e672ea15919bd9f55f9709ce3f2439e8b7a19 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 23:23:59 +0700 Subject: [PATCH 11/42] purchase bills table page --- .../apps/purchase/purchase-bills/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 3 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/purchase-bill.ts | 244 ++++++++++ src/types/apps/purchaseBillType.ts | 12 + .../list/PurchaseBillListTable.tsx | 460 ++++++++++++++++++ .../purchase/purchase-bills/list/index.tsx | 19 + 8 files changed, 749 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/page.tsx create mode 100644 src/data/dummy/purchase-bill.ts create mode 100644 src/types/apps/purchaseBillType.ts create mode 100644 src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx create mode 100644 src/views/apps/purchase/purchase-bills/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/page.tsx new file mode 100644 index 0000000..c90cb99 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/page.tsx @@ -0,0 +1,7 @@ +import PurchaseBillList from '@/views/apps/purchase/purchase-bills/list' + +const PurchaseBillsPage = () => { + return +} + +export default PurchaseBillsPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index e1fe92b..c2b2ee5 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -97,6 +97,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].overview} + + {dictionary['navigation'].purchase_bills} + {dictionary['navigation'].purchase_orders} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index f153a38..adaf149 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -115,6 +115,7 @@ "vendor": "Vendor", "sales": "Sales", "purchase_text": "Purchase", - "purchase_orders": "Purchase Orders" + "purchase_orders": "Purchase Orders", + "purchase_bills": "Purchase Bills" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 9ad2d0c..510a0d0 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -115,6 +115,7 @@ "vendor": "Vendor", "sales": "Penjualan", "purchase_text": "Pembelian", - "purchase_orders": "Pesanan Pembelian" + "purchase_orders": "Pesanan Pembelian", + "purchase_bills": "Tagihan Pembelian" } } diff --git a/src/data/dummy/purchase-bill.ts b/src/data/dummy/purchase-bill.ts new file mode 100644 index 0000000..a9ec2e3 --- /dev/null +++ b/src/data/dummy/purchase-bill.ts @@ -0,0 +1,244 @@ +import { PurchaseBillType } from '@/types/apps/purchaseBillType' + +export const purchaseBillsData: PurchaseBillType[] = [ + { + id: 1, + number: 'PB-001', + vendorName: 'Andi Wijaya', + vendorCompany: 'PT Sumber Makmur', + reference: 'REF-001', + date: '2025-09-01', + dueDate: '2025-09-15', + status: 'Belum Dibayar', + remainingBill: 5000000, + total: 5000000 + }, + { + id: 2, + number: 'PB-002', + vendorName: 'Siti Rahma', + vendorCompany: 'CV Cahaya Abadi', + reference: 'REF-002', + date: '2025-08-28', + dueDate: '2025-09-12', + status: 'Dibayar Sebagian', + remainingBill: 2000000, + total: 8000000 + }, + { + id: 3, + number: 'PB-003', + vendorName: 'Budi Santoso', + vendorCompany: 'UD Sejahtera', + reference: 'REF-003', + date: '2025-09-05', + dueDate: '2025-09-20', + status: 'Lunas', + remainingBill: 0, + total: 3500000 + }, + { + id: 4, + number: 'PB-004', + vendorName: 'Rina Kartika', + vendorCompany: 'PT Mitra Jaya', + reference: 'REF-004', + date: '2025-08-30', + dueDate: '2025-09-14', + status: 'Belum Dibayar', + remainingBill: 12000000, + total: 12000000 + }, + { + id: 5, + number: 'PB-005', + vendorName: 'Agus Salim', + vendorCompany: 'CV Bumi Persada', + reference: 'REF-005', + date: '2025-09-03', + dueDate: '2025-09-18', + status: 'Dibayar Sebagian', + remainingBill: 1500000, + total: 6000000 + }, + { + id: 6, + number: 'PB-006', + vendorName: 'Maya Lestari', + vendorCompany: 'PT Tunas Baru', + reference: 'REF-006', + date: '2025-09-06', + dueDate: '2025-09-21', + status: 'Lunas', + remainingBill: 0, + total: 9000000 + }, + { + id: 7, + number: 'PB-007', + vendorName: 'Hendra Gunawan', + vendorCompany: 'UD Prima Sentosa', + reference: 'REF-007', + date: '2025-09-02', + dueDate: '2025-09-17', + status: 'Belum Dibayar', + remainingBill: 7200000, + total: 7200000 + }, + { + id: 8, + number: 'PB-008', + vendorName: 'Dewi Anggraini', + vendorCompany: 'CV Inti Mandiri', + reference: 'REF-008', + date: '2025-08-27', + dueDate: '2025-09-11', + status: 'Dibayar Sebagian', + remainingBill: 3000000, + total: 10000000 + }, + { + id: 9, + number: 'PB-009', + vendorName: 'Yusuf Arifin', + vendorCompany: 'PT Surya Kencana', + reference: 'REF-009', + date: '2025-09-04', + dueDate: '2025-09-19', + status: 'Lunas', + remainingBill: 0, + total: 4500000 + }, + { + id: 10, + number: 'PB-010', + vendorName: 'Nurhayati', + vendorCompany: 'UD Cahaya Mulia', + reference: 'REF-010', + date: '2025-09-07', + dueDate: '2025-09-22', + status: 'Belum Dibayar', + remainingBill: 2500000, + total: 2500000 + }, + { + id: 11, + number: 'PB-011', + vendorName: 'Fajar Hidayat', + vendorCompany: 'PT Bina Karya', + reference: 'REF-011', + date: '2025-09-01', + dueDate: '2025-09-16', + status: 'Dibayar Sebagian', + remainingBill: 1000000, + total: 7000000 + }, + { + id: 12, + number: 'PB-012', + vendorName: 'Ratna Sari', + vendorCompany: 'CV Mega Utama', + reference: 'REF-012', + date: '2025-09-08', + dueDate: '2025-09-23', + status: 'Lunas', + remainingBill: 0, + total: 5600000 + }, + { + id: 13, + number: 'PB-013', + vendorName: 'Tono Prasetyo', + vendorCompany: 'UD Karya Indah', + reference: 'REF-013', + date: '2025-08-29', + dueDate: '2025-09-13', + status: 'Belum Dibayar', + remainingBill: 8000000, + total: 8000000 + }, + { + id: 14, + number: 'PB-014', + vendorName: 'Lina Marlina', + vendorCompany: 'PT Harmoni Sejati', + reference: 'REF-014', + date: '2025-09-05', + dueDate: '2025-09-20', + status: 'Dibayar Sebagian', + remainingBill: 4000000, + total: 10000000 + }, + { + id: 15, + number: 'PB-015', + vendorName: 'Arman Saputra', + vendorCompany: 'CV Sentra Niaga', + reference: 'REF-015', + date: '2025-09-03', + dueDate: '2025-09-18', + status: 'Lunas', + remainingBill: 0, + total: 3000000 + }, + { + id: 16, + number: 'PB-016', + vendorName: 'Indah Permata', + vendorCompany: 'PT Citra Abadi', + reference: 'REF-016', + date: '2025-08-31', + dueDate: '2025-09-15', + status: 'Belum Dibayar', + remainingBill: 6700000, + total: 6700000 + }, + { + id: 17, + number: 'PB-017', + vendorName: 'Adi Putra', + vendorCompany: 'UD Makmur Bersama', + reference: 'REF-017', + date: '2025-09-02', + dueDate: '2025-09-17', + status: 'Dibayar Sebagian', + remainingBill: 2000000, + total: 9000000 + }, + { + id: 18, + number: 'PB-018', + vendorName: 'Sri Wahyuni', + vendorCompany: 'CV Bintang Terang', + reference: 'REF-018', + date: '2025-09-06', + dueDate: '2025-09-21', + status: 'Lunas', + remainingBill: 0, + total: 7200000 + }, + { + id: 19, + number: 'PB-019', + vendorName: 'Eko Prabowo', + vendorCompany: 'PT Mandiri Jaya', + reference: 'REF-019', + date: '2025-09-04', + dueDate: '2025-09-19', + status: 'Belum Dibayar', + remainingBill: 11000000, + total: 11000000 + }, + { + id: 20, + number: 'PB-020', + vendorName: 'Novi Astuti', + vendorCompany: 'UD Sinar Harapan', + reference: 'REF-020', + date: '2025-09-07', + dueDate: '2025-09-22', + status: 'Dibayar Sebagian', + remainingBill: 500000, + total: 4500000 + } +] diff --git a/src/types/apps/purchaseBillType.ts b/src/types/apps/purchaseBillType.ts new file mode 100644 index 0000000..886a294 --- /dev/null +++ b/src/types/apps/purchaseBillType.ts @@ -0,0 +1,12 @@ +export type PurchaseBillType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + dueDate: string + status: string + remainingBill: number + total: number +} diff --git a/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx new file mode 100644 index 0000000..488c854 --- /dev/null +++ b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx @@ -0,0 +1,460 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { PurchaseBillType } from '@/types/apps/purchaseBillType' +import { purchaseBillsData } from '@/data/dummy/purchase-bill' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type PurchaseBillTypeWithAction = PurchaseBillType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Purchase Bills +const getStatusColor = (status: string) => { + switch (status) { + case 'Belum Dibayar': + return 'error' + case 'Dibayar Sebagian': + return 'warning' + case 'Lunas': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const PurchaseBillListTable = () => { + const dispatch = useDispatch() + + // States + const [addBillOpen, setAddBillOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [billId, setBillId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(purchaseBillsData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = purchaseBillsData + + // Filter by search + if (search) { + filtered = filtered.filter( + bill => + bill.number.toLowerCase().includes(search.toLowerCase()) || + bill.vendorName.toLowerCase().includes(search.toLowerCase()) || + bill.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + bill.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(bill => bill.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleBillClick = (billId: string) => { + console.log('Navigasi ke detail Bill:', billId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Bill', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Vendor', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('remainingBill', { + header: 'Sisa Tagihan', + cell: ({ row }) => ( + 0 ? 'error.main' : 'success.main'}> + {formatCurrency(row.original.remainingBill)} + + ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as PurchaseBillType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas', 'Lainnya'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Purchase Bill' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default PurchaseBillListTable diff --git a/src/views/apps/purchase/purchase-bills/list/index.tsx b/src/views/apps/purchase/purchase-bills/list/index.tsx new file mode 100644 index 0000000..5dd9c75 --- /dev/null +++ b/src/views/apps/purchase/purchase-bills/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import PurchaseBillListTable from './PurchaseBillListTable' + +const PurchaseBillList = () => { + return ( + + + + + + ) +} + +export default PurchaseBillList From d0d5d066a187254fae02c380443c19d658d87a33 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 23:28:17 +0700 Subject: [PATCH 12/42] purchase bill add page --- .../apps/purchase/purchase-bills/add/page.tsx | 19 +++++++++++++++++++ .../add/PurchaseBillAddHeader.tsx | 19 +++++++++++++++++++ .../list/PurchaseBillListTable.tsx | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/add/page.tsx create mode 100644 src/views/apps/purchase/purchase-bills/add/PurchaseBillAddHeader.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/add/page.tsx new file mode 100644 index 0000000..f718bb7 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/add/page.tsx @@ -0,0 +1,19 @@ +import Grid from '@mui/material/Grid2' + +import PurchaseAddForm from '@/views/apps/purchase/purchase-form/PurchaseAddForm' +import PurchaseBillAddHeader from '@/views/apps/purchase/purchase-bills/add/PurchaseBillAddHeader' + +const PurchaseBillAddPage = () => { + return ( + + + + + + + + + ) +} + +export default PurchaseBillAddPage diff --git a/src/views/apps/purchase/purchase-bills/add/PurchaseBillAddHeader.tsx b/src/views/apps/purchase/purchase-bills/add/PurchaseBillAddHeader.tsx new file mode 100644 index 0000000..9e94a63 --- /dev/null +++ b/src/views/apps/purchase/purchase-bills/add/PurchaseBillAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const PurchaseBillAddHeader = () => { + return ( +
+
+ + Tambah Tagihan Pembelian + +
+
+ ) +} + +export default PurchaseBillAddHeader diff --git a/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx index 488c854..09f9d44 100644 --- a/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx +++ b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx @@ -377,7 +377,7 @@ const PurchaseBillListTable = () => { component={Link} className='max-sm:is-full is-auto' startIcon={} - href={getLocalizedUrl('/apps/purchase/bills/add', locale as Locale)} + href={getLocalizedUrl('/apps/purchase/purchase-bills/add', locale as Locale)} > Tambah From 9b787e65d49b2fdae9aba44bb5369052fd0f44f9 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 23:41:04 +0700 Subject: [PATCH 13/42] purchase delivery table page --- .../purchase/purchase-deliveries/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 3 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/purchase-delivery.ts | 184 ++++++++ src/types/apps/purchaseDeliveryTypes.ts | 9 + .../list/PurchaseDeliveryListTable.tsx | 424 ++++++++++++++++++ .../purchase-deliveries/list/index.tsx | 19 + 8 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-deliveries/page.tsx create mode 100644 src/data/dummy/purchase-delivery.ts create mode 100644 src/types/apps/purchaseDeliveryTypes.ts create mode 100644 src/views/apps/purchase/purchase-deliveries/list/PurchaseDeliveryListTable.tsx create mode 100644 src/views/apps/purchase/purchase-deliveries/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-deliveries/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-deliveries/page.tsx new file mode 100644 index 0000000..33d18f1 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-deliveries/page.tsx @@ -0,0 +1,7 @@ +import PurchaseDeliveryListTable from '@/views/apps/purchase/purchase-deliveries/list/PurchaseDeliveryListTable' + +const PurchaseDeliveryPage = () => { + return +} + +export default PurchaseDeliveryPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index c2b2ee5..006555c 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -100,6 +100,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].purchase_bills} + + {dictionary['navigation'].purchase_delivery} + {dictionary['navigation'].purchase_orders} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index adaf149..06b04d4 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -116,6 +116,7 @@ "sales": "Sales", "purchase_text": "Purchase", "purchase_orders": "Purchase Orders", - "purchase_bills": "Purchase Bills" + "purchase_bills": "Purchase Bills", + "purchase_delivery": "Purchase Delivery" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 510a0d0..3529862 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -116,6 +116,7 @@ "sales": "Penjualan", "purchase_text": "Pembelian", "purchase_orders": "Pesanan Pembelian", - "purchase_bills": "Tagihan Pembelian" + "purchase_bills": "Tagihan Pembelian", + "purchase_delivery": "Pengiriman Pembelian" } } diff --git a/src/data/dummy/purchase-delivery.ts b/src/data/dummy/purchase-delivery.ts new file mode 100644 index 0000000..d6a5645 --- /dev/null +++ b/src/data/dummy/purchase-delivery.ts @@ -0,0 +1,184 @@ +import { PurchaseDeliveryType } from '@/types/apps/purchaseDeliveryTypes' + +export const purchaseDeliveryData: PurchaseDeliveryType[] = [ + { + id: 1, + number: 'PD-001', + vendorName: 'Andi Wijaya', + vendorCompany: 'PT Sumber Makmur', + reference: 'REF-PD-001', + date: '2025-09-01', + status: 'Open' + }, + { + id: 2, + number: 'PD-002', + vendorName: 'Siti Rahma', + vendorCompany: 'CV Cahaya Abadi', + reference: 'REF-PD-002', + date: '2025-09-02', + status: 'Selesai' + }, + { + id: 3, + number: 'PD-003', + vendorName: 'Budi Santoso', + vendorCompany: 'UD Sejahtera', + reference: 'REF-PD-003', + date: '2025-09-03', + status: 'Open' + }, + { + id: 4, + number: 'PD-004', + vendorName: 'Rina Kartika', + vendorCompany: 'PT Mitra Jaya', + reference: 'REF-PD-004', + date: '2025-09-04', + status: 'Selesai' + }, + { + id: 5, + number: 'PD-005', + vendorName: 'Agus Salim', + vendorCompany: 'CV Bumi Persada', + reference: 'REF-PD-005', + date: '2025-09-05', + status: 'Open' + }, + { + id: 6, + number: 'PD-006', + vendorName: 'Maya Lestari', + vendorCompany: 'PT Tunas Baru', + reference: 'REF-PD-006', + date: '2025-09-06', + status: 'Selesai' + }, + { + id: 7, + number: 'PD-007', + vendorName: 'Hendra Gunawan', + vendorCompany: 'UD Prima Sentosa', + reference: 'REF-PD-007', + date: '2025-09-07', + status: 'Open' + }, + { + id: 8, + number: 'PD-008', + vendorName: 'Dewi Anggraini', + vendorCompany: 'CV Inti Mandiri', + reference: 'REF-PD-008', + date: '2025-09-08', + status: 'Selesai' + }, + { + id: 9, + number: 'PD-009', + vendorName: 'Yusuf Arifin', + vendorCompany: 'PT Surya Kencana', + reference: 'REF-PD-009', + date: '2025-09-09', + status: 'Open' + }, + { + id: 10, + number: 'PD-010', + vendorName: 'Nurhayati', + vendorCompany: 'UD Cahaya Mulia', + reference: 'REF-PD-010', + date: '2025-09-10', + status: 'Selesai' + }, + { + id: 11, + number: 'PD-011', + vendorName: 'Fajar Hidayat', + vendorCompany: 'PT Bina Karya', + reference: 'REF-PD-011', + date: '2025-09-11', + status: 'Open' + }, + { + id: 12, + number: 'PD-012', + vendorName: 'Ratna Sari', + vendorCompany: 'CV Mega Utama', + reference: 'REF-PD-012', + date: '2025-09-12', + status: 'Selesai' + }, + { + id: 13, + number: 'PD-013', + vendorName: 'Tono Prasetyo', + vendorCompany: 'UD Karya Indah', + reference: 'REF-PD-013', + date: '2025-09-13', + status: 'Open' + }, + { + id: 14, + number: 'PD-014', + vendorName: 'Lina Marlina', + vendorCompany: 'PT Harmoni Sejati', + reference: 'REF-PD-014', + date: '2025-09-14', + status: 'Selesai' + }, + { + id: 15, + number: 'PD-015', + vendorName: 'Arman Saputra', + vendorCompany: 'CV Sentra Niaga', + reference: 'REF-PD-015', + date: '2025-09-15', + status: 'Open' + }, + { + id: 16, + number: 'PD-016', + vendorName: 'Indah Permata', + vendorCompany: 'PT Citra Abadi', + reference: 'REF-PD-016', + date: '2025-09-16', + status: 'Selesai' + }, + { + id: 17, + number: 'PD-017', + vendorName: 'Adi Putra', + vendorCompany: 'UD Makmur Bersama', + reference: 'REF-PD-017', + date: '2025-09-17', + status: 'Open' + }, + { + id: 18, + number: 'PD-018', + vendorName: 'Sri Wahyuni', + vendorCompany: 'CV Bintang Terang', + reference: 'REF-PD-018', + date: '2025-09-18', + status: 'Selesai' + }, + { + id: 19, + number: 'PD-019', + vendorName: 'Eko Prabowo', + vendorCompany: 'PT Mandiri Jaya', + reference: 'REF-PD-019', + date: '2025-09-19', + status: 'Open' + }, + { + id: 20, + number: 'PD-020', + vendorName: 'Novi Astuti', + vendorCompany: 'UD Sinar Harapan', + reference: 'REF-PD-020', + date: '2025-09-20', + status: 'Selesai' + } +] diff --git a/src/types/apps/purchaseDeliveryTypes.ts b/src/types/apps/purchaseDeliveryTypes.ts new file mode 100644 index 0000000..b5a859c --- /dev/null +++ b/src/types/apps/purchaseDeliveryTypes.ts @@ -0,0 +1,9 @@ +export type PurchaseDeliveryType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + status: string +} diff --git a/src/views/apps/purchase/purchase-deliveries/list/PurchaseDeliveryListTable.tsx b/src/views/apps/purchase/purchase-deliveries/list/PurchaseDeliveryListTable.tsx new file mode 100644 index 0000000..4c0c4b5 --- /dev/null +++ b/src/views/apps/purchase/purchase-deliveries/list/PurchaseDeliveryListTable.tsx @@ -0,0 +1,424 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { PurchaseDeliveryType } from '@/types/apps/purchaseDeliveryTypes' +import { purchaseDeliveryData } from '@/data/dummy/purchase-delivery' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type PurchaseDeliveryTypeWithAction = PurchaseDeliveryType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Purchase Delivery +const getStatusColor = (status: string) => { + switch (status) { + case 'Open': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const PurchaseDeliveryListTable = () => { + const dispatch = useDispatch() + + // States + const [addDeliveryOpen, setAddDeliveryOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [deliveryId, setDeliveryId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(purchaseDeliveryData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = purchaseDeliveryData + + // Filter by search + if (search) { + filtered = filtered.filter( + delivery => + delivery.number.toLowerCase().includes(search.toLowerCase()) || + delivery.vendorName.toLowerCase().includes(search.toLowerCase()) || + delivery.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + delivery.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(delivery => delivery.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleDeliveryClick = (deliveryId: string) => { + console.log('Navigasi ke detail Delivery:', deliveryId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Delivery', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Vendor', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as PurchaseDeliveryType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Open', 'Selesai'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Purchase Delivery' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default PurchaseDeliveryListTable diff --git a/src/views/apps/purchase/purchase-deliveries/list/index.tsx b/src/views/apps/purchase/purchase-deliveries/list/index.tsx new file mode 100644 index 0000000..33dbb8e --- /dev/null +++ b/src/views/apps/purchase/purchase-deliveries/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import PurchaseDeliveryListTable from './PurchaseDeliveryListTable' + +const PurchaseDeliveryList = () => { + return ( + + + + + + ) +} + +export default PurchaseDeliveryList From f585f23c54e0d7cf066f5a548a7d0bc4ca2f9eb8 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 9 Sep 2025 23:55:38 +0700 Subject: [PATCH 14/42] purchase quote list and add --- .../purchase/purchase-quotes/add/page.tsx | 19 + .../apps/purchase/purchase-quotes/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 3 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/purchase-quote.ts | 224 +++++++++ src/types/apps/purchaseQuoteTypes.ts | 11 + .../add/PurchaseQuoteAddHeader.tsx | 19 + .../list/PurchaseQuoteListTable.tsx | 452 ++++++++++++++++++ .../purchase/purchase-quote/list/index.tsx | 19 + 10 files changed, 758 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/add/page.tsx create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/page.tsx create mode 100644 src/data/dummy/purchase-quote.ts create mode 100644 src/types/apps/purchaseQuoteTypes.ts create mode 100644 src/views/apps/purchase/purchase-quote/add/PurchaseQuoteAddHeader.tsx create mode 100644 src/views/apps/purchase/purchase-quote/list/PurchaseQuoteListTable.tsx create mode 100644 src/views/apps/purchase/purchase-quote/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/add/page.tsx new file mode 100644 index 0000000..a38dead --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/add/page.tsx @@ -0,0 +1,19 @@ +import Grid from '@mui/material/Grid2' + +import PurchaseAddForm from '@/views/apps/purchase/purchase-form/PurchaseAddForm' +import PurchaseQuoteAddHeader from '@/views/apps/purchase/purchase-quote/add/PurchaseQuoteAddHeader' + +const PurchaseQuoteAddPage = () => { + return ( + + + + + + + + + ) +} + +export default PurchaseQuoteAddPage diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/page.tsx new file mode 100644 index 0000000..f77e3f6 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-quotes/page.tsx @@ -0,0 +1,7 @@ +import PurchaseQuoteList from '@/views/apps/purchase/purchase-quote/list' + +const PurchaseQuotePage = () => { + return +} + +export default PurchaseQuotePage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 006555c..0de51bc 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -106,6 +106,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].purchase_orders} + + {dictionary['navigation'].purchase_quotes} +
}> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 06b04d4..63e0ece 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -117,6 +117,7 @@ "purchase_text": "Purchase", "purchase_orders": "Purchase Orders", "purchase_bills": "Purchase Bills", - "purchase_delivery": "Purchase Delivery" + "purchase_delivery": "Purchase Delivery", + "purchase_quotes": "Purchase Quotes" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 3529862..ecdc0a1 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -117,6 +117,7 @@ "purchase_text": "Pembelian", "purchase_orders": "Pesanan Pembelian", "purchase_bills": "Tagihan Pembelian", - "purchase_delivery": "Pengiriman Pembelian" + "purchase_delivery": "Pengiriman Pembelian", + "purchase_quotes": "Penawaran Pembelian" } } diff --git a/src/data/dummy/purchase-quote.ts b/src/data/dummy/purchase-quote.ts new file mode 100644 index 0000000..d7ae7cd --- /dev/null +++ b/src/data/dummy/purchase-quote.ts @@ -0,0 +1,224 @@ +import { PurchaseQuoteType } from '@/types/apps/purchaseQuoteTypes' + +export const purchaseQuoteData: PurchaseQuoteType[] = [ + { + id: 1, + number: 'PQ-001', + vendorName: 'Andi Wijaya', + vendorCompany: 'PT Sumber Makmur', + reference: 'REF-PQ-001', + date: '2025-09-01', + dueDate: '2025-09-10', + status: 'Open', + total: 4500000 + }, + { + id: 2, + number: 'PQ-002', + vendorName: 'Siti Rahma', + vendorCompany: 'CV Cahaya Abadi', + reference: 'REF-PQ-002', + date: '2025-09-02', + dueDate: '2025-09-12', + status: 'Selesai', + total: 7200000 + }, + { + id: 3, + number: 'PQ-003', + vendorName: 'Budi Santoso', + vendorCompany: 'UD Sejahtera', + reference: 'REF-PQ-003', + date: '2025-09-03', + dueDate: '2025-09-13', + status: 'Dipesan Sebagai', + total: 3100000 + }, + { + id: 4, + number: 'PQ-004', + vendorName: 'Rina Kartika', + vendorCompany: 'PT Mitra Jaya', + reference: 'REF-PQ-004', + date: '2025-09-04', + dueDate: '2025-09-15', + status: 'Open', + total: 5800000 + }, + { + id: 5, + number: 'PQ-005', + vendorName: 'Agus Salim', + vendorCompany: 'CV Bumi Persada', + reference: 'REF-PQ-005', + date: '2025-09-05', + dueDate: '2025-09-16', + status: 'Selesai', + total: 8000000 + }, + { + id: 6, + number: 'PQ-006', + vendorName: 'Maya Lestari', + vendorCompany: 'PT Tunas Baru', + reference: 'REF-PQ-006', + date: '2025-09-06', + dueDate: '2025-09-17', + status: 'Dipesan Sebagai', + total: 2600000 + }, + { + id: 7, + number: 'PQ-007', + vendorName: 'Hendra Gunawan', + vendorCompany: 'UD Prima Sentosa', + reference: 'REF-PQ-007', + date: '2025-09-07', + dueDate: '2025-09-18', + status: 'Open', + total: 9300000 + }, + { + id: 8, + number: 'PQ-008', + vendorName: 'Dewi Anggraini', + vendorCompany: 'CV Inti Mandiri', + reference: 'REF-PQ-008', + date: '2025-09-08', + dueDate: '2025-09-19', + status: 'Selesai', + total: 4100000 + }, + { + id: 9, + number: 'PQ-009', + vendorName: 'Yusuf Arifin', + vendorCompany: 'PT Surya Kencana', + reference: 'REF-PQ-009', + date: '2025-09-09', + dueDate: '2025-09-20', + status: 'Dipesan Sebagai', + total: 6900000 + }, + { + id: 10, + number: 'PQ-010', + vendorName: 'Nurhayati', + vendorCompany: 'UD Cahaya Mulia', + reference: 'REF-PQ-010', + date: '2025-09-10', + dueDate: '2025-09-21', + status: 'Open', + total: 5500000 + }, + { + id: 11, + number: 'PQ-011', + vendorName: 'Fajar Hidayat', + vendorCompany: 'PT Bina Karya', + reference: 'REF-PQ-011', + date: '2025-09-11', + dueDate: '2025-09-22', + status: 'Selesai', + total: 12000000 + }, + { + id: 12, + number: 'PQ-012', + vendorName: 'Ratna Sari', + vendorCompany: 'CV Mega Utama', + reference: 'REF-PQ-012', + date: '2025-09-12', + dueDate: '2025-09-23', + status: 'Dipesan Sebagai', + total: 3300000 + }, + { + id: 13, + number: 'PQ-013', + vendorName: 'Tono Prasetyo', + vendorCompany: 'UD Karya Indah', + reference: 'REF-PQ-013', + date: '2025-09-13', + dueDate: '2025-09-24', + status: 'Open', + total: 7500000 + }, + { + id: 14, + number: 'PQ-014', + vendorName: 'Lina Marlina', + vendorCompany: 'PT Harmoni Sejati', + reference: 'REF-PQ-014', + date: '2025-09-14', + dueDate: '2025-09-25', + status: 'Selesai', + total: 8600000 + }, + { + id: 15, + number: 'PQ-015', + vendorName: 'Arman Saputra', + vendorCompany: 'CV Sentra Niaga', + reference: 'REF-PQ-015', + date: '2025-09-15', + dueDate: '2025-09-26', + status: 'Dipesan Sebagai', + total: 2950000 + }, + { + id: 16, + number: 'PQ-016', + vendorName: 'Indah Permata', + vendorCompany: 'PT Citra Abadi', + reference: 'REF-PQ-016', + date: '2025-09-16', + dueDate: '2025-09-27', + status: 'Open', + total: 6800000 + }, + { + id: 17, + number: 'PQ-017', + vendorName: 'Adi Putra', + vendorCompany: 'UD Makmur Bersama', + reference: 'REF-PQ-017', + date: '2025-09-17', + dueDate: '2025-09-28', + status: 'Selesai', + total: 4700000 + }, + { + id: 18, + number: 'PQ-018', + vendorName: 'Sri Wahyuni', + vendorCompany: 'CV Bintang Terang', + reference: 'REF-PQ-018', + date: '2025-09-18', + dueDate: '2025-09-29', + status: 'Dipesan Sebagai', + total: 5300000 + }, + { + id: 19, + number: 'PQ-019', + vendorName: 'Eko Prabowo', + vendorCompany: 'PT Mandiri Jaya', + reference: 'REF-PQ-019', + date: '2025-09-19', + dueDate: '2025-09-30', + status: 'Open', + total: 9500000 + }, + { + id: 20, + number: 'PQ-020', + vendorName: 'Novi Astuti', + vendorCompany: 'UD Sinar Harapan', + reference: 'REF-PQ-020', + date: '2025-09-20', + dueDate: '2025-10-01', + status: 'Selesai', + total: 4200000 + } +] diff --git a/src/types/apps/purchaseQuoteTypes.ts b/src/types/apps/purchaseQuoteTypes.ts new file mode 100644 index 0000000..72fb878 --- /dev/null +++ b/src/types/apps/purchaseQuoteTypes.ts @@ -0,0 +1,11 @@ +export type PurchaseQuoteType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + dueDate: string + status: string + total: number +} diff --git a/src/views/apps/purchase/purchase-quote/add/PurchaseQuoteAddHeader.tsx b/src/views/apps/purchase/purchase-quote/add/PurchaseQuoteAddHeader.tsx new file mode 100644 index 0000000..239107e --- /dev/null +++ b/src/views/apps/purchase/purchase-quote/add/PurchaseQuoteAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const PurchaseQuoteAddHeader = () => { + return ( +
+
+ + Tambah Penawaran Pembelian + +
+
+ ) +} + +export default PurchaseQuoteAddHeader diff --git a/src/views/apps/purchase/purchase-quote/list/PurchaseQuoteListTable.tsx b/src/views/apps/purchase/purchase-quote/list/PurchaseQuoteListTable.tsx new file mode 100644 index 0000000..6217168 --- /dev/null +++ b/src/views/apps/purchase/purchase-quote/list/PurchaseQuoteListTable.tsx @@ -0,0 +1,452 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { PurchaseQuoteType } from '@/types/apps/purchaseQuoteTypes' +import { purchaseQuoteData } from '@/data/dummy/purchase-quote' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type PurchaseQuoteTypeWithAction = PurchaseQuoteType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Purchase Quote +const getStatusColor = (status: string) => { + switch (status) { + case 'Open': + return 'primary' + case 'Dipesan Sebagian': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const PurchaseQuoteListTable = () => { + const dispatch = useDispatch() + + // States + const [addQuoteOpen, setAddQuoteOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [quoteId, setQuoteId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(purchaseQuoteData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = purchaseQuoteData + + // Filter by search + if (search) { + filtered = filtered.filter( + quote => + quote.number.toLowerCase().includes(search.toLowerCase()) || + quote.vendorName.toLowerCase().includes(search.toLowerCase()) || + quote.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + quote.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(quote => quote.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleQuoteClick = (quoteId: string) => { + console.log('Navigasi ke detail Quote:', quoteId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Quote', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Vendor', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as PurchaseQuoteType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Open', 'Selesai', 'Dipesan Sebagian'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Purchase Quote' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default PurchaseQuoteListTable diff --git a/src/views/apps/purchase/purchase-quote/list/index.tsx b/src/views/apps/purchase/purchase-quote/list/index.tsx new file mode 100644 index 0000000..b5c041a --- /dev/null +++ b/src/views/apps/purchase/purchase-quote/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import PurchaseQuoteListTable from './PurchaseQuoteListTable' + +const PurchaseQuoteList = () => { + return ( + + + + + + ) +} + +export default PurchaseQuoteList From cc3a0c07a93f53b6e98a7cbec87128280279c47b Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 00:19:11 +0700 Subject: [PATCH 15/42] Sales Invoice Page --- .../(private)/apps/sales/sales-bills/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 1 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/sales.ts | 244 +++++++++ src/types/apps/salesTypes.ts | 12 + .../sales-bill/list/SalesBillListTable.tsx | 464 ++++++++++++++++++ .../apps/sales/sales-bill/list/index.tsx | 19 + 8 files changed, 751 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx create mode 100644 src/data/dummy/sales.ts create mode 100644 src/types/apps/salesTypes.ts create mode 100644 src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx create mode 100644 src/views/apps/sales/sales-bill/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx new file mode 100644 index 0000000..314d4d3 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-bills/page.tsx @@ -0,0 +1,7 @@ +import SalesBillList from '@/views/apps/sales/sales-bill/list' + +const SalesBillPage = () => { + return +} + +export default SalesBillPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 0de51bc..7e30be1 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -93,6 +93,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].overview} + {dictionary['navigation'].invoices} {/* {dictionary['navigation'].view} */} }> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 63e0ece..86399b1 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -118,6 +118,7 @@ "purchase_orders": "Purchase Orders", "purchase_bills": "Purchase Bills", "purchase_delivery": "Purchase Delivery", - "purchase_quotes": "Purchase Quotes" + "purchase_quotes": "Purchase Quotes", + "invoices": "Invoices" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index ecdc0a1..9126d73 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -118,6 +118,7 @@ "purchase_orders": "Pesanan Pembelian", "purchase_bills": "Tagihan Pembelian", "purchase_delivery": "Pengiriman Pembelian", - "purchase_quotes": "Penawaran Pembelian" + "purchase_quotes": "Penawaran Pembelian", + "invoices": "Tagihan" } } diff --git a/src/data/dummy/sales.ts b/src/data/dummy/sales.ts new file mode 100644 index 0000000..72f4452 --- /dev/null +++ b/src/data/dummy/sales.ts @@ -0,0 +1,244 @@ +import { SalesBillType } from '@/types/apps/salesTypes' + +export const salesBillData: SalesBillType[] = [ + { + id: 1, + number: 'INV/001', + customerName: 'Andi Wijaya', + customerCompany: 'PT Nusantara Abadi', + reference: 'REF-SB-001', + date: '2025-09-01', + dueDate: '2025-09-15', + status: 'Belum Dibayar', + remainingBill: 5000000, + total: 5000000 + }, + { + id: 2, + number: 'INV/002', + customerName: 'Siti Rahma', + customerCompany: 'CV Cahaya Abadi', + reference: 'REF-SB-002', + date: '2025-09-02', + dueDate: '2025-09-16', + status: 'Dibayar Sebagian', + remainingBill: 2000000, + total: 8000000 + }, + { + id: 3, + number: 'INV/003', + customerName: 'Budi Santoso', + customerCompany: 'UD Sejahtera', + reference: 'REF-SB-003', + date: '2025-09-03', + dueDate: '2025-09-17', + status: 'Lunas', + remainingBill: 0, + total: 3500000 + }, + { + id: 4, + number: 'INV/004', + customerName: 'Rina Kartika', + customerCompany: 'PT Mitra Jaya', + reference: 'REF-SB-004', + date: '2025-09-04', + dueDate: '2025-09-18', + status: 'Void', + remainingBill: 0, + total: 4200000 + }, + { + id: 5, + number: 'INV/005', + customerName: 'Agus Salim', + customerCompany: 'CV Bumi Persada', + reference: 'REF-SB-005', + date: '2025-09-05', + dueDate: '2025-09-19', + status: 'Retur', + remainingBill: 0, + total: 6100000 + }, + { + id: 6, + number: 'INV/006', + customerName: 'Maya Lestari', + customerCompany: 'PT Tunas Baru', + reference: 'REF-SB-006', + date: '2025-09-06', + dueDate: '2025-09-20', + status: 'Belum Dibayar', + remainingBill: 9000000, + total: 9000000 + }, + { + id: 7, + number: 'INV/007', + customerName: 'Hendra Gunawan', + customerCompany: 'UD Prima Sentosa', + reference: 'REF-SB-007', + date: '2025-09-07', + dueDate: '2025-09-21', + status: 'Dibayar Sebagian', + remainingBill: 1500000, + total: 7500000 + }, + { + id: 8, + number: 'INV/008', + customerName: 'Dewi Anggraini', + customerCompany: 'CV Inti Mandiri', + reference: 'REF-SB-008', + date: '2025-09-08', + dueDate: '2025-09-22', + status: 'Lunas', + remainingBill: 0, + total: 4500000 + }, + { + id: 9, + number: 'INV/009', + customerName: 'Yusuf Arifin', + customerCompany: 'PT Surya Kencana', + reference: 'REF-SB-009', + date: '2025-09-09', + dueDate: '2025-09-23', + status: 'Void', + remainingBill: 0, + total: 5000000 + }, + { + id: 10, + number: 'INV/010', + customerName: 'Nurhayati', + customerCompany: 'UD Cahaya Mulia', + reference: 'REF-SB-010', + date: '2025-09-10', + dueDate: '2025-09-24', + status: 'Retur', + remainingBill: 0, + total: 3000000 + }, + { + id: 11, + number: 'INV/011', + customerName: 'Fajar Hidayat', + customerCompany: 'PT Bina Karya', + reference: 'REF-SB-011', + date: '2025-09-11', + dueDate: '2025-09-25', + status: 'Belum Dibayar', + remainingBill: 6000000, + total: 6000000 + }, + { + id: 12, + number: 'INV/012', + customerName: 'Ratna Sari', + customerCompany: 'CV Mega Utama', + reference: 'REF-SB-012', + date: '2025-09-12', + dueDate: '2025-09-26', + status: 'Dibayar Sebagian', + remainingBill: 2500000, + total: 10000000 + }, + { + id: 13, + number: 'INV/013', + customerName: 'Tono Prasetyo', + customerCompany: 'UD Karya Indah', + reference: 'REF-SB-013', + date: '2025-09-13', + dueDate: '2025-09-27', + status: 'Lunas', + remainingBill: 0, + total: 7000000 + }, + { + id: 14, + number: 'INV/014', + customerName: 'Lina Marlina', + customerCompany: 'PT Harmoni Sejati', + reference: 'REF-SB-014', + date: '2025-09-14', + dueDate: '2025-09-28', + status: 'Void', + remainingBill: 0, + total: 3200000 + }, + { + id: 15, + number: 'INV/015', + customerName: 'Arman Saputra', + customerCompany: 'CV Sentra Niaga', + reference: 'REF-SB-015', + date: '2025-09-15', + dueDate: '2025-09-29', + status: 'Retur', + remainingBill: 0, + total: 5400000 + }, + { + id: 16, + number: 'INV/016', + customerName: 'Indah Permata', + customerCompany: 'PT Citra Abadi', + reference: 'REF-SB-016', + date: '2025-09-16', + dueDate: '2025-09-30', + status: 'Belum Dibayar', + remainingBill: 8000000, + total: 8000000 + }, + { + id: 17, + number: 'INV/017', + customerName: 'Adi Putra', + customerCompany: 'UD Makmur Bersama', + reference: 'REF-SB-017', + date: '2025-09-17', + dueDate: '2025-10-01', + status: 'Dibayar Sebagian', + remainingBill: 1200000, + total: 6200000 + }, + { + id: 18, + number: 'INV/018', + customerName: 'Sri Wahyuni', + customerCompany: 'CV Bintang Terang', + reference: 'REF-SB-018', + date: '2025-09-18', + dueDate: '2025-10-02', + status: 'Lunas', + remainingBill: 0, + total: 9200000 + }, + { + id: 19, + number: 'INV/019', + customerName: 'Eko Prabowo', + customerCompany: 'PT Mandiri Jaya', + reference: 'REF-SB-019', + date: '2025-09-19', + dueDate: '2025-10-03', + status: 'Void', + remainingBill: 0, + total: 2500000 + }, + { + id: 20, + number: 'INV/020', + customerName: 'Novi Astuti', + customerCompany: 'UD Sinar Harapan', + reference: 'REF-SB-020', + date: '2025-09-20', + dueDate: '2025-10-04', + status: 'Retur', + remainingBill: 0, + total: 4800000 + } +] diff --git a/src/types/apps/salesTypes.ts b/src/types/apps/salesTypes.ts new file mode 100644 index 0000000..0c57b4e --- /dev/null +++ b/src/types/apps/salesTypes.ts @@ -0,0 +1,12 @@ +export type SalesBillType = { + id: number + number: string + customerName: string + customerCompany: string + reference: string + date: string + dueDate: string + status: string + remainingBill: number + total: number +} diff --git a/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx new file mode 100644 index 0000000..6245605 --- /dev/null +++ b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx @@ -0,0 +1,464 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { SalesBillType } from '@/types/apps/salesTypes' +import { salesBillData } from '@/data/dummy/sales' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type SalesBillTypeWithAction = SalesBillType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Sales Bill +const getStatusColor = (status: string) => { + switch (status) { + case 'Belum Dibayar': + return 'error' + case 'Dibayar Sebagian': + return 'warning' + case 'Lunas': + return 'success' + case 'Void': + return 'default' + case 'Retur': + return 'info' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const SalesBillListTable = () => { + const dispatch = useDispatch() + + // States + const [addBillOpen, setAddBillOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [billId, setBillId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(salesBillData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = salesBillData + + // Filter by search + if (search) { + filtered = filtered.filter( + bill => + bill.number.toLowerCase().includes(search.toLowerCase()) || + bill.customerName.toLowerCase().includes(search.toLowerCase()) || + bill.customerCompany.toLowerCase().includes(search.toLowerCase()) || + bill.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(bill => bill.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleBillClick = (billId: string) => { + console.log('Navigasi ke detail Sales Bill:', billId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Bill', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('customerName', { + header: 'Customer', + cell: ({ row }) => ( +
+ + {row.original.customerName} + + + {row.original.customerCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('remainingBill', { + header: 'Sisa Tagihan', + cell: ({ row }) => ( + 0 ? 'text-red-600' : 'text-green-600'}`}> + {formatCurrency(row.original.remainingBill)} + + ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as SalesBillType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas', 'Void', 'Retur'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Sales Bill' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default SalesBillListTable diff --git a/src/views/apps/sales/sales-bill/list/index.tsx b/src/views/apps/sales/sales-bill/list/index.tsx new file mode 100644 index 0000000..777f7be --- /dev/null +++ b/src/views/apps/sales/sales-bill/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import SalesBillListTable from './SalesBillListTable' + +const SalesBillList = () => { + return ( + + + + + + ) +} + +export default SalesBillList From ce9f845fe44855c3560907a1cecaa62d12825592 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 00:28:23 +0700 Subject: [PATCH 16/42] Sales Delivery Page --- .../apps/sales/sales-deliveries/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 1 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/sales.ts | 185 +++++++- src/types/apps/salesTypes.ts | 10 + .../list/SalesDeliveryListTable.tsx | 424 ++++++++++++++++++ .../sales/sales-deliveries/list/index.tsx | 19 + 8 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/sales-deliveries/page.tsx create mode 100644 src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx create mode 100644 src/views/apps/sales/sales-deliveries/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-deliveries/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-deliveries/page.tsx new file mode 100644 index 0000000..8036a1c --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-deliveries/page.tsx @@ -0,0 +1,7 @@ +import SalesDeliveryListTable from '@/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable' + +const SalesDeliveryPage = () => { + return +} + +export default SalesDeliveryPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 7e30be1..0afa81b 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -94,6 +94,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].overview} {dictionary['navigation'].invoices} + {dictionary['navigation'].deliveries} {/* {dictionary['navigation'].view} */} }> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 86399b1..7ddc720 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -119,6 +119,7 @@ "purchase_bills": "Purchase Bills", "purchase_delivery": "Purchase Delivery", "purchase_quotes": "Purchase Quotes", - "invoices": "Invoices" + "invoices": "Invoices", + "deliveries": "Deliveries" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 9126d73..bc87362 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -119,6 +119,7 @@ "purchase_bills": "Tagihan Pembelian", "purchase_delivery": "Pengiriman Pembelian", "purchase_quotes": "Penawaran Pembelian", - "invoices": "Tagihan" + "invoices": "Tagihan", + "deliveries": "Pengiriman" } } diff --git a/src/data/dummy/sales.ts b/src/data/dummy/sales.ts index 72f4452..d1dabdc 100644 --- a/src/data/dummy/sales.ts +++ b/src/data/dummy/sales.ts @@ -1,4 +1,4 @@ -import { SalesBillType } from '@/types/apps/salesTypes' +import { SalesBillType, SalesDeliveryType } from '@/types/apps/salesTypes' export const salesBillData: SalesBillType[] = [ { @@ -242,3 +242,186 @@ export const salesBillData: SalesBillType[] = [ total: 4800000 } ] + +export const salesDeliveryData: SalesDeliveryType[] = [ + { + id: 1, + number: 'INV-001', + vendorName: 'Andi Wijaya', + vendorCompany: 'PT Sumber Makmur', + reference: 'REF-INV-001', + date: '2025-09-01', + status: 'Open' + }, + { + id: 2, + number: 'INV-002', + vendorName: 'Siti Rahma', + vendorCompany: 'CV Cahaya Abadi', + reference: 'REF-INV-002', + date: '2025-09-02', + status: 'Selesai' + }, + { + id: 3, + number: 'INV-003', + vendorName: 'Budi Santoso', + vendorCompany: 'UD Sejahtera', + reference: 'REF-INV-003', + date: '2025-09-03', + status: 'Open' + }, + { + id: 4, + number: 'INV-004', + vendorName: 'Rina Kartika', + vendorCompany: 'PT Mitra Jaya', + reference: 'REF-INV-004', + date: '2025-09-04', + status: 'Selesai' + }, + { + id: 5, + number: 'INV-005', + vendorName: 'Agus Salim', + vendorCompany: 'CV Bumi Persada', + reference: 'REF-INV-005', + date: '2025-09-05', + status: 'Open' + }, + { + id: 6, + number: 'INV-006', + vendorName: 'Maya Lestari', + vendorCompany: 'PT Tunas Baru', + reference: 'REF-INV-006', + date: '2025-09-06', + status: 'Selesai' + }, + { + id: 7, + number: 'INV-007', + vendorName: 'Hendra Gunawan', + vendorCompany: 'UD Prima Sentosa', + reference: 'REF-INV-007', + date: '2025-09-07', + status: 'Open' + }, + { + id: 8, + number: 'INV-008', + vendorName: 'Dewi Anggraini', + vendorCompany: 'CV Inti Mandiri', + reference: 'REF-INV-008', + date: '2025-09-08', + status: 'Selesai' + }, + { + id: 9, + number: 'INV-009', + vendorName: 'Yusuf Arifin', + vendorCompany: 'PT Surya Kencana', + reference: 'REF-INV-009', + date: '2025-09-09', + status: 'Open' + }, + { + id: 10, + number: 'INV-010', + vendorName: 'Nurhayati', + vendorCompany: 'UD Cahaya Mulia', + reference: 'REF-INV-010', + date: '2025-09-10', + status: 'Selesai' + }, + { + id: 11, + number: 'INV-011', + vendorName: 'Fajar Hidayat', + vendorCompany: 'PT Bina Karya', + reference: 'REF-INV-011', + date: '2025-09-11', + status: 'Open' + }, + { + id: 12, + number: 'INV-012', + vendorName: 'Ratna Sari', + vendorCompany: 'CV Mega Utama', + reference: 'REF-INV-012', + date: '2025-09-12', + status: 'Selesai' + }, + { + id: 13, + number: 'INV-013', + vendorName: 'Tono Prasetyo', + vendorCompany: 'UD Karya Indah', + reference: 'REF-INV-013', + date: '2025-09-13', + status: 'Open' + }, + { + id: 14, + number: 'INV-014', + vendorName: 'Lina Marlina', + vendorCompany: 'PT Harmoni Sejati', + reference: 'REF-INV-014', + date: '2025-09-14', + status: 'Selesai' + }, + { + id: 15, + number: 'INV-015', + vendorName: 'Arman Saputra', + vendorCompany: 'CV Sentra Niaga', + reference: 'REF-INV-015', + date: '2025-09-15', + status: 'Open' + }, + { + id: 16, + number: 'INV-016', + vendorName: 'Indah Permata', + vendorCompany: 'PT Citra Abadi', + reference: 'REF-INV-016', + date: '2025-09-16', + status: 'Selesai' + }, + { + id: 17, + number: 'INV-017', + vendorName: 'Adi Putra', + vendorCompany: 'UD Makmur Bersama', + reference: 'REF-INV-017', + date: '2025-09-17', + status: 'Open' + }, + { + id: 18, + number: 'INV-018', + vendorName: 'Sri Wahyuni', + vendorCompany: 'CV Bintang Terang', + reference: 'REF-INV-018', + date: '2025-09-18', + status: 'Selesai' + }, + { + id: 19, + number: 'INV-019', + vendorName: 'Eko Prabowo', + vendorCompany: 'PT Mandiri Jaya', + reference: 'REF-INV-019', + date: '2025-09-19', + status: 'Open' + }, + { + id: 20, + number: 'INV-020', + vendorName: 'Novi Astuti', + vendorCompany: 'UD Sinar Harapan', + reference: 'REF-INV-020', + date: '2025-09-20', + status: 'Selesai' + } +] diff --git a/src/types/apps/salesTypes.ts b/src/types/apps/salesTypes.ts index 0c57b4e..527cdd5 100644 --- a/src/types/apps/salesTypes.ts +++ b/src/types/apps/salesTypes.ts @@ -10,3 +10,13 @@ export type SalesBillType = { remainingBill: number total: number } + +export type SalesDeliveryType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + status: string +} diff --git a/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx b/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx new file mode 100644 index 0000000..6a2faf4 --- /dev/null +++ b/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx @@ -0,0 +1,424 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { SalesDeliveryType } from '@/types/apps/salesTypes' +import { salesDeliveryData } from '@/data/dummy/sales' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type SalesDeliveryTypeWithAction = SalesDeliveryType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Sales Delivery +const getStatusColor = (status: string) => { + switch (status) { + case 'Open': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const SalesDeliveryListTable = () => { + const dispatch = useDispatch() + + // States + const [addDeliveryOpen, setAddDeliveryOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [deliveryId, setDeliveryId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(salesDeliveryData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = salesDeliveryData + + // Filter by search + if (search) { + filtered = filtered.filter( + delivery => + delivery.number.toLowerCase().includes(search.toLowerCase()) || + delivery.vendorName.toLowerCase().includes(search.toLowerCase()) || + delivery.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + delivery.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(delivery => delivery.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleDeliveryClick = (deliveryId: string) => { + console.log('Navigasi ke detail Delivery:', deliveryId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Delivery', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Vendor', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as SalesDeliveryType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Open', 'Selesai'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Sales Delivery' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default SalesDeliveryListTable diff --git a/src/views/apps/sales/sales-deliveries/list/index.tsx b/src/views/apps/sales/sales-deliveries/list/index.tsx new file mode 100644 index 0000000..99a2c8a --- /dev/null +++ b/src/views/apps/sales/sales-deliveries/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import SalesDeliveryListTable from './SalesDeliveryListTable' + +const SalesDeliveryList = () => { + return ( + + + + + + ) +} + +export default SalesDeliveryList From 8ec6ba62f3a57e00989bc5ede21eec9c492c0aa0 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 00:40:54 +0700 Subject: [PATCH 17/42] Sales Order List table --- .../apps/sales/sales-orders/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 1 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/sales.ts | 225 ++++++++- src/types/apps/salesTypes.ts | 12 + .../list/SalesDeliveryListTable.tsx | 2 +- .../sales-orders/list/SalesOrderListTable.tsx | 454 ++++++++++++++++++ .../apps/sales/sales-orders/list/index.tsx | 19 + 9 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/sales-orders/page.tsx create mode 100644 src/views/apps/sales/sales-orders/list/SalesOrderListTable.tsx create mode 100644 src/views/apps/sales/sales-orders/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-orders/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-orders/page.tsx new file mode 100644 index 0000000..5c694ba --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-orders/page.tsx @@ -0,0 +1,7 @@ +import SalesOrderList from '@/views/apps/sales/sales-orders/list' + +const SalesOrdersPage = () => { + return +} + +export default SalesOrdersPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 0afa81b..fcabc32 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -95,6 +95,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].overview} {dictionary['navigation'].invoices} {dictionary['navigation'].deliveries} + {dictionary['navigation'].sales_orders} {/* {dictionary['navigation'].view} */}
}> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 7ddc720..77b0b1d 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -120,6 +120,7 @@ "purchase_delivery": "Purchase Delivery", "purchase_quotes": "Purchase Quotes", "invoices": "Invoices", - "deliveries": "Deliveries" + "deliveries": "Deliveries", + "sales_orders": "Orders" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index bc87362..b174d83 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -120,6 +120,7 @@ "purchase_delivery": "Pengiriman Pembelian", "purchase_quotes": "Penawaran Pembelian", "invoices": "Tagihan", - "deliveries": "Pengiriman" + "deliveries": "Pengiriman", + "sales_orders": "Pemesanan" } } diff --git a/src/data/dummy/sales.ts b/src/data/dummy/sales.ts index d1dabdc..f207300 100644 --- a/src/data/dummy/sales.ts +++ b/src/data/dummy/sales.ts @@ -1,4 +1,4 @@ -import { SalesBillType, SalesDeliveryType } from '@/types/apps/salesTypes' +import { SalesBillType, SalesDeliveryType, SalesOrderType } from '@/types/apps/salesTypes' export const salesBillData: SalesBillType[] = [ { @@ -425,3 +425,226 @@ export const salesDeliveryData: SalesDeliveryType[] = [ status: 'Selesai' } ] + +export const salesOrdersData: SalesOrderType[] = [ + { + id: 1, + number: 'PO/00040', + vendorName: 'Kairav Wijaya Nababan', + vendorCompany: 'Yayasan Haryanto Tbk', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Disetujui', + total: 2037170 + }, + { + id: 2, + number: 'PO/00041', + vendorName: 'Sari Melani Hutapea', + vendorCompany: 'PT Santoso Jaya', + reference: 'REF-2025-001', + date: '03/09/2025', + dueDate: '17/09/2025', + status: 'Draft', + total: 1875500 + }, + { + id: 3, + number: 'PO/00042', + vendorName: 'Budi Prasetyo Manullang', + vendorCompany: 'CV Wijaya Makmur', + reference: 'REF-2025-002', + date: '07/09/2025', + dueDate: '21/09/2025', + status: 'Dikirim Sebagian', + total: 3250750 + }, + { + id: 4, + number: 'PO/00043', + vendorName: 'Maya Sari Lumban Gaol', + vendorCompany: 'PT Indah Permata', + reference: '', + date: '02/09/2025', + dueDate: '16/09/2025', + status: 'Selesai', + total: 980250 + }, + { + id: 5, + number: 'PO/00044', + vendorName: 'Rahman Hakim Siahaan', + vendorCompany: 'Toko Bahagia Sejahtera', + reference: 'REF-2025-003', + date: '08/09/2025', + dueDate: '22/09/2025', + status: 'Draft', + total: 4125890 + }, + { + id: 6, + number: 'PO/00045', + vendorName: 'Dewi Anggraini Panjaitan', + vendorCompany: 'PT Maju Bersama', + reference: 'REF-2025-004', + date: '01/09/2025', + dueDate: '15/09/2025', + status: 'Selesai', + total: 2678300 + }, + { + id: 7, + number: 'PO/00046', + vendorName: 'Agung Wijaya Simbolon', + vendorCompany: 'CV Karya Mandiri', + reference: '', + date: '06/09/2025', + dueDate: '20/09/2025', + status: 'Disetujui', + total: 1456780 + }, + { + id: 8, + number: 'PO/00047', + vendorName: 'Fitri Handayani Sitorus', + vendorCompany: 'PT Global Nusantara', + reference: 'REF-2025-005', + date: '04/09/2025', + dueDate: '18/09/2025', + status: 'Dikirim Sebagian', + total: 5892450 + }, + { + id: 9, + number: 'PO/00048', + vendorName: 'Andi Setiawan Tampubolon', + vendorCompany: 'Yayasan Pembangunan Tbk', + reference: 'REF-2025-006', + date: '09/09/2025', + dueDate: '23/09/2025', + status: 'Draft', + total: 3567120 + }, + { + id: 10, + number: 'PO/00049', + vendorName: 'Rina Maharani Hutasoit', + vendorCompany: 'PT Sejahtera Abadi', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Disetujui', + total: 2234680 + }, + { + id: 11, + number: 'PO/00050', + vendorName: 'Joko Santoso Nainggolan', + vendorCompany: 'CV Berkah Jaya', + reference: 'REF-2025-007', + date: '03/09/2025', + dueDate: '17/09/2025', + status: 'Selesai', + total: 1789560 + }, + { + id: 12, + number: 'PO/00051', + vendorName: 'Linda Safitri Simanjuntak', + vendorCompany: 'PT Harapan Bangsa', + reference: 'REF-2025-008', + date: '07/09/2025', + dueDate: '21/09/2025', + status: 'Dikirim Sebagian', + total: 4321870 + }, + { + id: 13, + number: 'PO/00052', + vendorName: 'Irfan Maulana Pasaribu', + vendorCompany: 'CV Sumber Rejeki', + reference: 'REF-2025-009', + date: '10/09/2025', + dueDate: '24/09/2025', + status: 'Draft', + total: 2567890 + }, + { + id: 14, + number: 'PO/00053', + vendorName: 'Siska Permata Sinaga', + vendorCompany: 'PT Mitra Sukses', + reference: '', + date: '11/09/2025', + dueDate: '25/09/2025', + status: 'Disetujui', + total: 3456780 + }, + { + id: 15, + number: 'PO/00054', + vendorName: 'Rizky Aditya Siregar', + vendorCompany: 'Toko Aman Sentosa', + reference: 'REF-2025-010', + date: '08/09/2025', + dueDate: '22/09/2025', + status: 'Selesai', + total: 1234567 + }, + { + id: 16, + number: 'PO/00055', + vendorName: 'Nina Sari Hutabarat', + vendorCompany: 'PT Cahaya Terang', + reference: 'REF-2025-011', + date: '12/09/2025', + dueDate: '26/09/2025', + status: 'Dikirim Sebagian', + total: 5678900 + }, + { + id: 17, + number: 'PO/00056', + vendorName: 'Doni Prasetya Situmorang', + vendorCompany: 'CV Barokah Makmur', + reference: '', + date: '09/09/2025', + dueDate: '23/09/2025', + status: 'Draft', + total: 987654 + }, + { + id: 18, + number: 'PO/00057', + vendorName: 'Anita Dewi Marpaung', + vendorCompany: 'Yayasan Karya Bhakti', + reference: 'REF-2025-012', + date: '06/09/2025', + dueDate: '20/09/2025', + status: 'Disetujui', + total: 4567123 + }, + { + id: 19, + number: 'PO/00058', + vendorName: 'Tommy Wijaya Samosir', + vendorCompany: 'PT Bintang Timur', + reference: 'REF-2025-013', + date: '13/09/2025', + dueDate: '27/09/2025', + status: 'Selesai', + total: 2345678 + }, + { + id: 20, + number: 'PO/00059', + vendorName: 'Lestari Indah Pakpahan', + vendorCompany: 'CV Harmoni Jaya', + reference: '', + date: '05/09/2025', + dueDate: '19/09/2025', + status: 'Dikirim Sebagian', + total: 3789012 + } +] diff --git a/src/types/apps/salesTypes.ts b/src/types/apps/salesTypes.ts index 527cdd5..4685da0 100644 --- a/src/types/apps/salesTypes.ts +++ b/src/types/apps/salesTypes.ts @@ -20,3 +20,15 @@ export type SalesDeliveryType = { date: string status: string } + +export type SalesOrderType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + dueDate: string + status: string + total: number +} diff --git a/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx b/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx index 6a2faf4..1a0018b 100644 --- a/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx +++ b/src/views/apps/sales/sales-deliveries/list/SalesDeliveryListTable.tsx @@ -231,7 +231,7 @@ const SalesDeliveryListTable = () => { ) }), columnHelper.accessor('vendorName', { - header: 'Vendor', + header: 'Pelanggan', cell: ({ row }) => (
diff --git a/src/views/apps/sales/sales-orders/list/SalesOrderListTable.tsx b/src/views/apps/sales/sales-orders/list/SalesOrderListTable.tsx new file mode 100644 index 0000000..f5644b9 --- /dev/null +++ b/src/views/apps/sales/sales-orders/list/SalesOrderListTable.tsx @@ -0,0 +1,454 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { salesOrdersData } from '@/data/dummy/sales' +import { SalesOrderType } from '@/types/apps/salesTypes' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type SalesOrderTypeWithAction = SalesOrderType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping +const getStatusColor = (status: string) => { + switch (status) { + case 'Draft': + return 'secondary' + case 'Disetujui': + return 'primary' + case 'Dikirim Sebagian': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const SalesOrderListTable = () => { + const dispatch = useDispatch() + + // States + const [addPOOpen, setAddPOOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [poId, setPOId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(salesOrdersData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = salesOrdersData + + // Filter by search + if (search) { + filtered = filtered.filter( + po => + po.number.toLowerCase().includes(search.toLowerCase()) || + po.vendorName.toLowerCase().includes(search.toLowerCase()) || + po.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + po.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(po => po.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handlePOClick = (poId: string) => { + console.log('Navigasi ke detail PO:', poId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor PO', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Pelanggan', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as SalesOrderType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {['Semua', 'Open', 'Dikirim Sebagian', 'Selesai', 'Lainnya'].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Sales Order' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default SalesOrderListTable diff --git a/src/views/apps/sales/sales-orders/list/index.tsx b/src/views/apps/sales/sales-orders/list/index.tsx new file mode 100644 index 0000000..b70a222 --- /dev/null +++ b/src/views/apps/sales/sales-orders/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import SalesOrderListTable from './SalesOrderListTable' + +const SalesOrderList = () => { + return ( + + + + + + ) +} + +export default SalesOrderList From 8461e1f4b766656740fdc74c2ae6f28ab27baffd Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 00:47:15 +0700 Subject: [PATCH 18/42] Sales Quote List Table --- .../apps/sales/sales-quotes/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 2 +- src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/sales.ts | 225 ++++++++- src/types/apps/salesTypes.ts | 12 + .../sales-quote/list/SalesQuoteListTable.tsx | 460 ++++++++++++++++++ .../apps/sales/sales-quote/list/index.tsx | 19 + 8 files changed, 727 insertions(+), 4 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/sales/sales-quotes/page.tsx create mode 100644 src/views/apps/sales/sales-quote/list/SalesQuoteListTable.tsx create mode 100644 src/views/apps/sales/sales-quote/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-quotes/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-quotes/page.tsx new file mode 100644 index 0000000..78e9f78 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/sales/sales-quotes/page.tsx @@ -0,0 +1,7 @@ +import SalesQuoteList from '@/views/apps/sales/sales-quote/list' + +const SalesQuotePage = () => { + return +} + +export default SalesQuotePage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index fcabc32..56c1e2d 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -96,7 +96,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].invoices} {dictionary['navigation'].deliveries} {dictionary['navigation'].sales_orders} - {/* {dictionary['navigation'].view} */} + {dictionary['navigation'].quotes} }> {dictionary['navigation'].overview} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 77b0b1d..7c25d03 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -121,6 +121,7 @@ "purchase_quotes": "Purchase Quotes", "invoices": "Invoices", "deliveries": "Deliveries", - "sales_orders": "Orders" + "sales_orders": "Orders", + "quotes": "Quotes" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index b174d83..486f051 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -121,6 +121,7 @@ "purchase_quotes": "Penawaran Pembelian", "invoices": "Tagihan", "deliveries": "Pengiriman", - "sales_orders": "Pemesanan" + "sales_orders": "Pemesanan", + "quotes": "Penawaran" } } diff --git a/src/data/dummy/sales.ts b/src/data/dummy/sales.ts index f207300..dda7bee 100644 --- a/src/data/dummy/sales.ts +++ b/src/data/dummy/sales.ts @@ -1,4 +1,4 @@ -import { SalesBillType, SalesDeliveryType, SalesOrderType } from '@/types/apps/salesTypes' +import { SalesBillType, SalesDeliveryType, SalesOrderType, SalesQuoteType } from '@/types/apps/salesTypes' export const salesBillData: SalesBillType[] = [ { @@ -648,3 +648,226 @@ export const salesOrdersData: SalesOrderType[] = [ total: 3789012 } ] + +export const salesQuoteData: SalesQuoteType[] = [ + { + id: 1, + number: 'PQ-001', + vendorName: 'Andi Wijaya', + vendorCompany: 'PT Sumber Makmur', + reference: 'REF-PQ-001', + date: '2025-09-01', + dueDate: '2025-09-10', + status: 'Open', + total: 4500000 + }, + { + id: 2, + number: 'PQ-002', + vendorName: 'Siti Rahma', + vendorCompany: 'CV Cahaya Abadi', + reference: 'REF-PQ-002', + date: '2025-09-02', + dueDate: '2025-09-12', + status: 'Selesai', + total: 7200000 + }, + { + id: 3, + number: 'PQ-003', + vendorName: 'Budi Santoso', + vendorCompany: 'UD Sejahtera', + reference: 'REF-PQ-003', + date: '2025-09-03', + dueDate: '2025-09-13', + status: 'Dipesan Sebagai', + total: 3100000 + }, + { + id: 4, + number: 'PQ-004', + vendorName: 'Rina Kartika', + vendorCompany: 'PT Mitra Jaya', + reference: 'REF-PQ-004', + date: '2025-09-04', + dueDate: '2025-09-15', + status: 'Open', + total: 5800000 + }, + { + id: 5, + number: 'PQ-005', + vendorName: 'Agus Salim', + vendorCompany: 'CV Bumi Persada', + reference: 'REF-PQ-005', + date: '2025-09-05', + dueDate: '2025-09-16', + status: 'Selesai', + total: 8000000 + }, + { + id: 6, + number: 'PQ-006', + vendorName: 'Maya Lestari', + vendorCompany: 'PT Tunas Baru', + reference: 'REF-PQ-006', + date: '2025-09-06', + dueDate: '2025-09-17', + status: 'Dipesan Sebagai', + total: 2600000 + }, + { + id: 7, + number: 'PQ-007', + vendorName: 'Hendra Gunawan', + vendorCompany: 'UD Prima Sentosa', + reference: 'REF-PQ-007', + date: '2025-09-07', + dueDate: '2025-09-18', + status: 'Open', + total: 9300000 + }, + { + id: 8, + number: 'PQ-008', + vendorName: 'Dewi Anggraini', + vendorCompany: 'CV Inti Mandiri', + reference: 'REF-PQ-008', + date: '2025-09-08', + dueDate: '2025-09-19', + status: 'Selesai', + total: 4100000 + }, + { + id: 9, + number: 'PQ-009', + vendorName: 'Yusuf Arifin', + vendorCompany: 'PT Surya Kencana', + reference: 'REF-PQ-009', + date: '2025-09-09', + dueDate: '2025-09-20', + status: 'Dipesan Sebagai', + total: 6900000 + }, + { + id: 10, + number: 'PQ-010', + vendorName: 'Nurhayati', + vendorCompany: 'UD Cahaya Mulia', + reference: 'REF-PQ-010', + date: '2025-09-10', + dueDate: '2025-09-21', + status: 'Open', + total: 5500000 + }, + { + id: 11, + number: 'PQ-011', + vendorName: 'Fajar Hidayat', + vendorCompany: 'PT Bina Karya', + reference: 'REF-PQ-011', + date: '2025-09-11', + dueDate: '2025-09-22', + status: 'Selesai', + total: 12000000 + }, + { + id: 12, + number: 'PQ-012', + vendorName: 'Ratna Sari', + vendorCompany: 'CV Mega Utama', + reference: 'REF-PQ-012', + date: '2025-09-12', + dueDate: '2025-09-23', + status: 'Dipesan Sebagai', + total: 3300000 + }, + { + id: 13, + number: 'PQ-013', + vendorName: 'Tono Prasetyo', + vendorCompany: 'UD Karya Indah', + reference: 'REF-PQ-013', + date: '2025-09-13', + dueDate: '2025-09-24', + status: 'Open', + total: 7500000 + }, + { + id: 14, + number: 'PQ-014', + vendorName: 'Lina Marlina', + vendorCompany: 'PT Harmoni Sejati', + reference: 'REF-PQ-014', + date: '2025-09-14', + dueDate: '2025-09-25', + status: 'Selesai', + total: 8600000 + }, + { + id: 15, + number: 'PQ-015', + vendorName: 'Arman Saputra', + vendorCompany: 'CV Sentra Niaga', + reference: 'REF-PQ-015', + date: '2025-09-15', + dueDate: '2025-09-26', + status: 'Dipesan Sebagai', + total: 2950000 + }, + { + id: 16, + number: 'PQ-016', + vendorName: 'Indah Permata', + vendorCompany: 'PT Citra Abadi', + reference: 'REF-PQ-016', + date: '2025-09-16', + dueDate: '2025-09-27', + status: 'Open', + total: 6800000 + }, + { + id: 17, + number: 'PQ-017', + vendorName: 'Adi Putra', + vendorCompany: 'UD Makmur Bersama', + reference: 'REF-PQ-017', + date: '2025-09-17', + dueDate: '2025-09-28', + status: 'Selesai', + total: 4700000 + }, + { + id: 18, + number: 'PQ-018', + vendorName: 'Sri Wahyuni', + vendorCompany: 'CV Bintang Terang', + reference: 'REF-PQ-018', + date: '2025-09-18', + dueDate: '2025-09-29', + status: 'Dipesan Sebagai', + total: 5300000 + }, + { + id: 19, + number: 'PQ-019', + vendorName: 'Eko Prabowo', + vendorCompany: 'PT Mandiri Jaya', + reference: 'REF-PQ-019', + date: '2025-09-19', + dueDate: '2025-09-30', + status: 'Open', + total: 9500000 + }, + { + id: 20, + number: 'PQ-020', + vendorName: 'Novi Astuti', + vendorCompany: 'UD Sinar Harapan', + reference: 'REF-PQ-020', + date: '2025-09-20', + dueDate: '2025-10-01', + status: 'Selesai', + total: 4200000 + } +] diff --git a/src/types/apps/salesTypes.ts b/src/types/apps/salesTypes.ts index 4685da0..8d994fe 100644 --- a/src/types/apps/salesTypes.ts +++ b/src/types/apps/salesTypes.ts @@ -32,3 +32,15 @@ export type SalesOrderType = { status: string total: number } + +export type SalesQuoteType = { + id: number + number: string + vendorName: string + vendorCompany: string + reference: string + date: string + dueDate: string + status: string + total: number +} diff --git a/src/views/apps/sales/sales-quote/list/SalesQuoteListTable.tsx b/src/views/apps/sales/sales-quote/list/SalesQuoteListTable.tsx new file mode 100644 index 0000000..c88c88c --- /dev/null +++ b/src/views/apps/sales/sales-quote/list/SalesQuoteListTable.tsx @@ -0,0 +1,460 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { salesQuoteData } from '@/data/dummy/sales' +import { SalesQuoteType } from '@/types/apps/salesTypes' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type SalesQuoteTypeWithAction = SalesQuoteType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Sales Quote +const getStatusColor = (status: string) => { + switch (status) { + case 'Open': + return 'primary' + case 'Dipesan Sebagian': + return 'warning' + case 'Selesai': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const SalesQuoteListTable = () => { + const dispatch = useDispatch() + + // States + const [addQuoteOpen, setAddQuoteOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [quoteId, setQuoteId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(salesQuoteData) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = salesQuoteData + + // Filter by search + if (search) { + filtered = filtered.filter( + quote => + quote.number.toLowerCase().includes(search.toLowerCase()) || + quote.vendorName.toLowerCase().includes(search.toLowerCase()) || + quote.vendorCompany.toLowerCase().includes(search.toLowerCase()) || + quote.status.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(quote => quote.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleQuoteClick = (quoteId: string) => { + console.log('Navigasi ke detail Quote:', quoteId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Quote', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('vendorName', { + header: 'Pelanggan', + cell: ({ row }) => ( +
+ + {row.original.vendorName} + + + {row.original.vendorCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('dueDate', { + header: 'Tanggal Jatuh Tempo', + cell: ({ row }) => {row.original.dueDate} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as SalesQuoteType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs */} +
+
+ {[ + 'Semua', + 'Draft', + 'Terkirim', + 'Ditolak Pelanggan', + 'Disetujui Pelanggan', + 'Selesai', + 'Dipesan Sebagian' + ].map(status => ( + + ))} +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Sales Quote' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default SalesQuoteListTable diff --git a/src/views/apps/sales/sales-quote/list/index.tsx b/src/views/apps/sales/sales-quote/list/index.tsx new file mode 100644 index 0000000..6e1629a --- /dev/null +++ b/src/views/apps/sales/sales-quote/list/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import SalesQuoteListTable from './SalesQuoteListTable' + +const SalesQuoteList = () => { + return ( + + + + + + ) +} + +export default SalesQuoteList From c32d08666eee8b23ae2d30b57db2da27366277c4 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 03:23:54 +0700 Subject: [PATCH 19/42] Purchase Detail --- .../purchase-bills/[id]/detail/page.tsx | 18 + .../list/PurchaseBillListTable.tsx | 7 +- .../purchase-detail/PurchaseDetailContent.tsx | 26 ++ .../purchase-detail/PurchaseDetailHeader.tsx | 19 + .../PurchaseDetailInformation.tsx | 310 +++++++++++++ .../purchase-detail/PurchaseDetailLog.tsx | 59 +++ .../PurchaseDetailSendPayment.tsx | 417 ++++++++++++++++++ .../PurchaseDetailTransaction.tsx | 150 +++++++ 8 files changed, 1001 insertions(+), 5 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/[id]/detail/page.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailHeader.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailLog.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailSendPayment.tsx create mode 100644 src/views/apps/purchase/purchase-detail/PurchaseDetailTransaction.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/[id]/detail/page.tsx new file mode 100644 index 0000000..2b95d65 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/purchase/purchase-bills/[id]/detail/page.tsx @@ -0,0 +1,18 @@ +import PurchaseDetailContent from '@/views/apps/purchase/purchase-detail/PurchaseDetailContent' +import PurchaseDetailHeader from '@/views/apps/purchase/purchase-detail/PurchaseDetailHeader' +import Grid from '@mui/material/Grid2' + +const PurchaseBillDetailPage = () => { + return ( + + + + + + + + + ) +} + +export default PurchaseBillDetailPage diff --git a/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx index 09f9d44..199bc47 100644 --- a/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx +++ b/src/views/apps/purchase/purchase-bills/list/PurchaseBillListTable.tsx @@ -188,10 +188,6 @@ const PurchaseBillListTable = () => { setOpenConfirm(false) } - const handleBillClick = (billId: string) => { - console.log('Navigasi ke detail Bill:', billId) - } - const handleStatusFilter = (status: string) => { setStatusFilter(status) } @@ -227,7 +223,8 @@ const PurchaseBillListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' - onClick={() => handleBillClick(row.original.id.toString())} + component={Link} + href={getLocalizedUrl(`/apps/purchase/purchase-bills/${row.original.number}/detail`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500, diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx new file mode 100644 index 0000000..391a0b3 --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailContent.tsx @@ -0,0 +1,26 @@ +import Grid from '@mui/material/Grid2' +import PurchaseDetailInformation from './PurchaseDetailInformation' +import PurchaseDetailSendPayment from './PurchaseDetailSendPayment' +import PurchaseDetailLog from './PurchaseDetailLog' +import PurchaseDetailTransaction from './PurchaseDetailTransaction' + +const PurchaseDetailContent = () => { + return ( + + + + + + + + + + + + + + + ) +} + +export default PurchaseDetailContent diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailHeader.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailHeader.tsx new file mode 100644 index 0000000..26e56d9 --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailHeader.tsx @@ -0,0 +1,19 @@ +import { Typography } from '@mui/material' + +interface Props { + title: string +} + +const PurchaseDetailHeader = ({ title }: Props) => { + return ( +
+
+ + {title} + +
+
+ ) +} + +export default PurchaseDetailHeader diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx new file mode 100644 index 0000000..232b5c6 --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailInformation.tsx @@ -0,0 +1,310 @@ +import React from 'react' +import { + Card, + CardHeader, + CardContent, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Box, + Button, + IconButton +} from '@mui/material' +import Grid from '@mui/material/Grid2' + +interface Product { + produk: string + deskripsi: string + kuantitas: number + satuan: string + discount: string + harga: number + pajak: string + jumlah: number +} + +interface PurchaseData { + vendor: string + nomor: string + tglTransaksi: string + tglJatuhTempo: string + gudang: string + status: string +} + +const PurchaseDetailInformation: React.FC = () => { + const purchaseData: PurchaseData = { + vendor: 'Bagas Rizki Sihotang S.Farm Widodo', + nomor: 'PI/00053', + tglTransaksi: '08/09/2025', + tglJatuhTempo: '06/10/2025', + gudang: 'Unassigned', + status: 'Belum Dibayar' + } + + const products: Product[] = [ + { + produk: 'CB1 - Chelsea Boots', + deskripsi: 'Ukuran XS', + kuantitas: 3, + satuan: 'Pcs', + discount: '0%', + harga: 299000, + pajak: 'PPN', + jumlah: 897000 + }, + { + produk: 'CB1 - Chelsea Boots', + deskripsi: 'Ukuran M', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 299000, + pajak: 'PPN', + jumlah: 299000 + }, + { + produk: 'KH1 - Kneel High Boots', + deskripsi: 'Ukuran XL', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 299000, + pajak: 'PPN', + jumlah: 299000 + } + ] + + const totalKuantitas: number = products.reduce((sum, product) => sum + product.kuantitas, 0) + const subTotal: number = 1495000 + const ppn: number = 98670 + const total: number = 1593670 + const sisaTagihan: number = 1593670 + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + + Belum Dibayar + + + + + + + + + + } + /> + + + {/* Purchase Information */} + + + + + Vendor + + + {purchaseData.vendor} + + + + + + Tgl. Transaksi + + {purchaseData.tglTransaksi} + + + + + Gudang + + + {purchaseData.gudang} + + + + + + + + Nomor + + {purchaseData.nomor} + + + + + Tgl. Jatuh Tempo + + {purchaseData.tglJatuhTempo} + + + + + {/* Products Table */} + + + + + Produk + Deskripsi + Kuantitas + Satuan + Discount + Harga + Pajak + Jumlah + + + + {products.map((product, index) => ( + + + + {product.produk} + + + {product.deskripsi} + {product.kuantitas} + {product.satuan} + {product.discount} + {formatCurrency(product.harga)} + {product.pajak} + {formatCurrency(product.jumlah)} + + ))} + + {/* Total Kuantitas Row */} + + + Total Kuantitas + + + {totalKuantitas} + + + + + + + + +
+
+ + {/* Summary Section */} + + + {/* Empty space for left side */} + + + + + Sub Total + + + {formatCurrency(subTotal)} + + + + + + PPN + + + {formatCurrency(ppn)} + + + + + + Total + + + {formatCurrency(total)} + + + + + + Sisa Tagihan + + + {formatCurrency(sisaTagihan)} + + + + + + +
+
+ ) +} + +export default PurchaseDetailInformation diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailLog.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailLog.tsx new file mode 100644 index 0000000..20711ac --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailLog.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardHeader, Typography, Box, Link } from '@mui/material' + +interface LogEntry { + id: string + action: string + timestamp: string + user: string +} + +const PurchaseDetailLog: React.FC = () => { + const logEntries: LogEntry[] = [ + { + id: '1', + action: 'Terakhir diubah oleh', + timestamp: '08 Sep 2025 18:26', + user: 'pada' + } + ] + + return ( + + + Pantau log perubahan data +
+ } + sx={{ pb: 1 }} + /> + + {logEntries.map(entry => ( + + + + + {entry.action} {entry.user} {entry.timestamp} + + + + ))} + + + ) +} + +export default PurchaseDetailLog diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailSendPayment.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailSendPayment.tsx new file mode 100644 index 0000000..6f64a4a --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailSendPayment.tsx @@ -0,0 +1,417 @@ +'use client' + +import React, { useState } from 'react' +import { + Card, + CardContent, + Typography, + Button, + Box, + Accordion, + AccordionSummary, + AccordionDetails, + Tooltip, + IconButton, + CardHeader +} from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomTextField from '@/@core/components/mui/TextField' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' + +interface PaymentFormData { + totalDibayar: string + tglTransaksi: string + referensi: string + nomor: string + dibayarDari: string +} + +interface PemotonganItem { + id: string + dipotong: string + persentase: string + nominal: string + tipe: 'persen' | 'rupiah' +} + +const PurchaseDetailSendPayment: React.FC = () => { + const [formData, setFormData] = useState({ + totalDibayar: '849.000', + tglTransaksi: '10/09/2025', + referensi: '', + nomor: 'PP/00025', + dibayarDari: '1-10001 Kas' + }) + + const [expanded, setExpanded] = useState(false) + const [pemotonganItems, setPemotonganItems] = useState([]) + + const dibayarDariOptions = [ + { label: '1-10001 Kas', value: '1-10001 Kas' }, + { label: '1-10002 Bank BCA', value: '1-10002 Bank BCA' }, + { label: '1-10003 Bank Mandiri', value: '1-10003 Bank Mandiri' }, + { label: '1-10004 Petty Cash', value: '1-10004 Petty Cash' } + ] + + const pemotonganOptions = [ + { label: 'PPN 11%', value: 'ppn' }, + { label: 'PPh 21', value: 'pph21' }, + { label: 'PPh 23', value: 'pph23' }, + { label: 'Biaya Admin', value: 'admin' } + ] + + const handleChange = + (field: keyof PaymentFormData) => (event: React.ChangeEvent | any) => { + setFormData(prev => ({ + ...prev, + [field]: event.target.value + })) + } + + const handleDibayarDariChange = (value: { label: string; value: string } | null) => { + setFormData(prev => ({ + ...prev, + dibayarDari: value?.value || '' + })) + } + + const addPemotongan = () => { + const newItem: PemotonganItem = { + id: Date.now().toString(), + dipotong: '', + persentase: '0', + nominal: '', + tipe: 'persen' + } + setPemotonganItems(prev => [...prev, newItem]) + } + + const removePemotongan = (id: string) => { + setPemotonganItems(prev => prev.filter(item => item.id !== id)) + } + + const updatePemotongan = (id: string, field: keyof PemotonganItem, value: string) => { + setPemotonganItems(prev => prev.map(item => (item.id === id ? { ...item, [field]: value } : item))) + } + + const handleAccordionChange = () => { + setExpanded(!expanded) + } + + const calculatePemotongan = (item: PemotonganItem): number => { + const totalDibayar = parseInt(formData.totalDibayar.replace(/\D/g, '')) || 0 + const nilai = parseFloat(item.persentase) || 0 + + if (item.tipe === 'persen') { + return (totalDibayar * nilai) / 100 + } else { + return nilai + } + } + + const formatCurrency = (amount: string | number): string => { + const numAmount = typeof amount === 'string' ? parseInt(amount.replace(/\D/g, '')) : amount + return new Intl.NumberFormat('id-ID').format(numAmount) + } + + return ( + + + + Kirim Pembayaran + + + } + /> + + + {/* Left Column */} + + {/* Total Dibayar */} + + + * Total Dibayar + + } + value={formData.totalDibayar} + onChange={handleChange('totalDibayar')} + sx={{ + '& .MuiInputBase-root': { + textAlign: 'right' + } + }} + /> + + + {/* Tgl. Transaksi */} + + + * Tgl. Transaksi + + } + type='date' + value={formData.tglTransaksi.split('/').reverse().join('-')} + onChange={handleChange('tglTransaksi')} + slotProps={{ + input: { + endAdornment: + } + }} + /> + + + {/* Referensi */} + + + + Referensi + + + + + + + + + + + {/* Attachment Accordion */} + + + } + sx={{ + backgroundColor: '#f8f9fa', + borderRadius: '8px', + minHeight: '48px', + '& .MuiAccordionSummary-content': { + margin: '12px 0' + } + }} + > + + Attachment + + + + + Drag and drop files here or click to upload + + + + + + {/* Right Column */} + + {/* Nomor */} + + + + Nomor + + + + + + + + + + + {/* Dibayar Dari */} + + + * Dibayar Dari + + option.label || ''} + value={dibayarDariOptions.find(option => option.value === formData.dibayarDari) || null} + onChange={(_, value: { label: string; value: string } | null) => handleDibayarDariChange(value)} + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + {/* Empty space to match Referensi height */} + {/* Empty space */} + + {/* Pemotongan Button - aligned with Attachment */} + + + + + + + {/* Pemotongan Items */} + {pemotonganItems.length > 0 && ( + + {pemotonganItems.map((item, index) => ( + + + + removePemotongan(item.id)} + sx={{ + backgroundColor: '#fff', + border: '1px solid #f44336', + '&:hover': { backgroundColor: '#ffebee' } + }} + > + + + + + + option.label || ''} + value={pemotonganOptions.find(option => option.value === item.dipotong) || null} + onChange={(_, value: { label: string; value: string } | null) => + updatePemotongan(item.id, 'dipotong', value?.value || '') + } + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + + updatePemotongan(item.id, 'persentase', e.target.value)} + placeholder='0' + sx={{ + '& .MuiInputBase-root': { + textAlign: 'center' + } + }} + /> + + + + + + + + + + + + + {formatCurrency(calculatePemotongan(item))} + + + + + + ))} + + )} + + {/* Bottom Section */} + + + {/* Empty space */} + + + + Total + + + {formatCurrency(formData.totalDibayar)} + + + + + + + + + + ) +} + +export default PurchaseDetailSendPayment diff --git a/src/views/apps/purchase/purchase-detail/PurchaseDetailTransaction.tsx b/src/views/apps/purchase/purchase-detail/PurchaseDetailTransaction.tsx new file mode 100644 index 0000000..8be85b5 --- /dev/null +++ b/src/views/apps/purchase/purchase-detail/PurchaseDetailTransaction.tsx @@ -0,0 +1,150 @@ +'use client' + +import React from 'react' +import { + Card, + CardContent, + CardHeader, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Link, + Box +} from '@mui/material' + +interface TransactionData { + id: string + tanggal: string + transaksi: string + nomor: string + tag: string + referensi: string + jumlah: number +} + +const PurchaseDetailTransaction: React.FC = () => { + const transactions: TransactionData[] = [ + { + id: '1', + tanggal: '10/09/2025', + transaksi: 'Pembayaran Pembelian', + nomor: 'PP/00024', + tag: '', + referensi: '', + jumlah: 1593670 + } + ] + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + Transaksi + + } + sx={{ pb: 1 }} + /> + + + + + + + + + Tanggal + + + + + + + + Transaksi + + + + + + + + Nomor + + + + + + + Tag + + + + + Referensi + + + + + + Jumlah + + + + + + + + {transactions.map(transaction => ( + + + {transaction.tanggal} + + + + {transaction.transaksi} + + + + {transaction.nomor} + + + {transaction.tag || '-'} + + + {transaction.referensi || '-'} + + + + {formatCurrency(transaction.jumlah)} + + + + ))} + +
+
+
+
+ ) +} + +export default PurchaseDetailTransaction From 7077bf8d87bfdbbf0fd7aa20d90047edd5ede0d4 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 14:24:18 +0700 Subject: [PATCH 20/42] Expense List Table --- .../(private)/apps/expense/page.tsx | 7 + src/components/RangeDatePicker.tsx | 276 ++++++++++ src/components/StatusFilterTab.tsx | 249 +++++++++ .../layout/vertical/VerticalMenu.tsx | 9 +- src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/data/dummy/expense.ts | 224 ++++++++ src/services/api.ts | 3 +- src/types/apps/expenseType.ts | 11 + .../apps/expense/list/ExpenseListCard.tsx | 51 ++ .../apps/expense/list/ExpenseListTable.tsx | 519 ++++++++++++++++++ src/views/apps/expense/list/index.tsx | 23 + 12 files changed, 1372 insertions(+), 6 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/expense/page.tsx create mode 100644 src/components/RangeDatePicker.tsx create mode 100644 src/components/StatusFilterTab.tsx create mode 100644 src/data/dummy/expense.ts create mode 100644 src/types/apps/expenseType.ts create mode 100644 src/views/apps/expense/list/ExpenseListCard.tsx create mode 100644 src/views/apps/expense/list/ExpenseListTable.tsx create mode 100644 src/views/apps/expense/list/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/expense/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/expense/page.tsx new file mode 100644 index 0000000..08dd739 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/expense/page.tsx @@ -0,0 +1,7 @@ +import ExpenseList from '@/views/apps/expense/list' + +const ExpensePage = () => { + return +} + +export default ExpensePage diff --git a/src/components/RangeDatePicker.tsx b/src/components/RangeDatePicker.tsx new file mode 100644 index 0000000..774a0de --- /dev/null +++ b/src/components/RangeDatePicker.tsx @@ -0,0 +1,276 @@ +// React Imports +import React from 'react' + +// MUI Imports +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' + +interface DateRangePickerProps { + /** + * Start date value (Date object or date string) + */ + startDate: Date | string | null + /** + * End date value (Date object or date string) + */ + endDate: Date | string | null + /** + * Callback when start date changes + */ + onStartDateChange: (date: Date | null) => void + /** + * Callback when end date changes + */ + onEndDateChange: (date: Date | null) => void + /** + * Label for start date field + */ + startLabel?: string + /** + * Label for end date field + */ + endLabel?: string + /** + * Placeholder for start date field + */ + startPlaceholder?: string + /** + * Placeholder for end date field + */ + endPlaceholder?: string + /** + * Size of the text fields + */ + size?: 'small' | 'medium' + /** + * Whether the fields are disabled + */ + disabled?: boolean + /** + * Whether the fields are required + */ + required?: boolean + /** + * Custom className for the container + */ + className?: string + /** + * Custom styles for the container + */ + containerStyle?: React.CSSProperties + /** + * Separator between date fields + */ + separator?: string + /** + * Custom props for start date TextField + */ + startTextFieldProps?: Omit + /** + * Custom props for end date TextField + */ + endTextFieldProps?: Omit + /** + * Error state for start date + */ + startError?: boolean + /** + * Error state for end date + */ + endError?: boolean + /** + * Helper text for start date + */ + startHelperText?: string + /** + * Helper text for end date + */ + endHelperText?: string +} + +// Utility functions +const formatDateForInput = (date: Date | string | null): string => { + if (!date) return '' + + const dateObj = typeof date === 'string' ? new Date(date) : date + + if (isNaN(dateObj.getTime())) return '' + + return dateObj.toISOString().split('T')[0] +} + +const parseDateFromInput = (dateString: string): Date | null => { + if (!dateString) return null + + const date = new Date(dateString) + return isNaN(date.getTime()) ? null : date +} + +const DateRangePicker: React.FC = ({ + startDate, + endDate, + onStartDateChange, + onEndDateChange, + startLabel, + endLabel, + startPlaceholder, + endPlaceholder, + size = 'small', + disabled = false, + required = false, + className = '', + containerStyle = {}, + separator = '-', + startTextFieldProps = {}, + endTextFieldProps = {}, + startError = false, + endError = false, + startHelperText, + endHelperText +}) => { + const theme = useTheme() + + const defaultTextFieldSx = { + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: 'primary.main' + }, + '& fieldset': { + borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider + } + } + } + + const handleStartDateChange = (event: React.ChangeEvent) => { + const date = parseDateFromInput(event.target.value) + onStartDateChange(date) + } + + const handleEndDateChange = (event: React.ChangeEvent) => { + const date = parseDateFromInput(event.target.value) + onEndDateChange(date) + } + + return ( +
+ + + + {separator} + + + +
+ ) +} + +export default DateRangePicker + +// Export utility functions for external use +export { formatDateForInput, parseDateFromInput } + +// Example usage: +/* +import DateRangePicker from '@/components/DateRangePicker' + +// In your component: +const [startDate, setStartDate] = useState(new Date()) +const [endDate, setEndDate] = useState(new Date()) + +// Basic usage + + +// With labels and validation + endDate} + endError={startDate && endDate && startDate > endDate} + startHelperText={startDate && endDate && startDate > endDate ? "Tanggal mulai tidak boleh lebih besar dari tanggal selesai" : ""} +/> + +// Custom styling + + +// Integration with your existing filter logic +const handleDateRangeChange = (start: Date | null, end: Date | null) => { + setFilter({ + ...filter, + date_from: start ? formatDateDDMMYYYY(start) : null, + date_to: end ? formatDateDDMMYYYY(end) : null + }) +} + + handleDateRangeChange(date, filter.date_to ? new Date(filter.date_to) : null)} + onEndDateChange={(date) => handleDateRangeChange(filter.date_from ? new Date(filter.date_from) : null, date)} +/> +*/ diff --git a/src/components/StatusFilterTab.tsx b/src/components/StatusFilterTab.tsx new file mode 100644 index 0000000..861d2ae --- /dev/null +++ b/src/components/StatusFilterTab.tsx @@ -0,0 +1,249 @@ +// React Imports +import React, { useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' + +const DropdownButton = styled(Button)(({ theme }) => ({ + textTransform: 'none', + fontWeight: 400, + borderRadius: '8px', + borderColor: '#e0e0e0', + color: '#666', + '&:hover': { + borderColor: '#ccc', + backgroundColor: 'rgba(0, 0, 0, 0.04)' + } +})) + +interface StatusFilterTabsProps { + /** + * Array of status options to display as filter tabs + */ + statusOptions: string[] + /** + * Currently selected status filter + */ + selectedStatus: string + /** + * Callback function when a status is selected + */ + onStatusChange: (status: string) => void + /** + * Custom className for the container + */ + className?: string + /** + * Custom styles for the container + */ + containerStyle?: React.CSSProperties + /** + * Size of the buttons + */ + buttonSize?: 'small' | 'medium' | 'large' + /** + * Maximum number of status options to show as buttons before switching to dropdown + */ + maxButtonsBeforeDropdown?: number + /** + * Label for the dropdown when there are many options + */ + dropdownLabel?: string +} + +const StatusFilterTabs: React.FC = ({ + statusOptions, + selectedStatus, + onStatusChange, + className = '', + containerStyle = {}, + buttonSize = 'small', + maxButtonsBeforeDropdown = 5, + dropdownLabel = 'Lainnya' +}) => { + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + + const handleDropdownClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleDropdownClose = () => { + setAnchorEl(null) + } + + const handleDropdownItemClick = (status: string) => { + onStatusChange(status) + handleDropdownClose() + } + + // If status options are <= maxButtonsBeforeDropdown, show all as buttons + if (statusOptions.length <= maxButtonsBeforeDropdown) { + return ( +
+ {statusOptions.map(status => ( + + ))} +
+ ) + } + + // If more than maxButtonsBeforeDropdown, show first few as buttons and rest in dropdown + const buttonStatuses = statusOptions.slice(0, maxButtonsBeforeDropdown - 1) + const dropdownStatuses = statusOptions.slice(maxButtonsBeforeDropdown - 1) + const isDropdownItemSelected = dropdownStatuses.includes(selectedStatus) + + return ( +
+ {/* Regular buttons for first few statuses */} + {buttonStatuses.map(status => ( + + ))} + + {/* Dropdown button for remaining statuses */} + } + sx={{ + ...(isDropdownItemSelected && { + backgroundColor: 'primary.main', + color: 'primary.contrastText', + borderColor: 'primary.main', + fontWeight: 600, + '&:hover': { + backgroundColor: 'primary.dark', + borderColor: 'primary.dark' + } + }) + }} + > + {isDropdownItemSelected ? selectedStatus : dropdownLabel} + + + + {dropdownStatuses.map(status => ( + handleDropdownItemClick(status)} + selected={selectedStatus === status} + sx={{ + fontSize: '14px', + fontWeight: selectedStatus === status ? 600 : 400, + color: selectedStatus === status ? 'primary.main' : 'text.primary' + }} + > + {status} + + ))} + +
+ ) +} + +export default StatusFilterTabs + +// Example usage: +/* +import StatusFilterTabs from '@/components/StatusFilterTabs' + +// In your component: +const [statusFilter, setStatusFilter] = useState('Semua') + +// For few statuses (will show all as buttons) +const expenseStatusOptions = ['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas'] + +// For many statuses (will show some buttons + dropdown) +const manyStatusOptions = [ + 'Semua', + 'Belum Dibayar', + 'Dibayar Sebagian', + 'Lunas', + 'Void', + 'Retur', + 'Jatuh Tempo', + 'Transaksi Berulang', + 'Ditangguhkan', + 'Dibatalkan' +] + + + +// With many options (will automatically use dropdown) + + +// Custom configuration + +*/ diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 56c1e2d..007da5d 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -91,14 +91,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].dailyReport} - }> + }> {dictionary['navigation'].overview} {dictionary['navigation'].invoices} {dictionary['navigation'].deliveries} {dictionary['navigation'].sales_orders} {dictionary['navigation'].quotes} - }> + }> {dictionary['navigation'].overview} {dictionary['navigation'].purchase_bills} @@ -113,6 +113,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].purchase_quotes} + }> + {dictionary['navigation'].list} + }> {dictionary['navigation'].list} @@ -160,7 +163,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].list} {/* {dictionary['navigation'].view} */} - }> + }> {dictionary['navigation'].list} {/* {dictionary['navigation'].view} */} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index 7c25d03..c9ab714 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -122,6 +122,7 @@ "invoices": "Invoices", "deliveries": "Deliveries", "sales_orders": "Orders", - "quotes": "Quotes" + "quotes": "Quotes", + "expenses": "Expenses" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 486f051..5c4742b 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -122,6 +122,7 @@ "invoices": "Tagihan", "deliveries": "Pengiriman", "sales_orders": "Pemesanan", - "quotes": "Penawaran" + "quotes": "Penawaran", + "expenses": "Biaya" } } diff --git a/src/data/dummy/expense.ts b/src/data/dummy/expense.ts new file mode 100644 index 0000000..85c5ec1 --- /dev/null +++ b/src/data/dummy/expense.ts @@ -0,0 +1,224 @@ +import { ExpenseType } from '@/types/apps/expenseType' + +export const expenseData: ExpenseType[] = [ + { + id: 1, + date: '2025-01-05', + number: 'EXP/2025/001', + reference: 'REF-EXP-001', + benefeciaryName: 'Budi Santoso', + benefeciaryCompany: 'PT Maju Jaya', + status: 'Belum Dibayar', + balanceDue: 5000000, + total: 5000000 + }, + { + id: 2, + date: '2025-01-06', + number: 'EXP/2025/002', + reference: 'REF-EXP-002', + benefeciaryName: 'Siti Aminah', + benefeciaryCompany: 'CV Sentosa Abadi', + status: 'Dibayar Sebagian', + balanceDue: 2500000, + total: 6000000 + }, + { + id: 3, + date: '2025-01-07', + number: 'EXP/2025/003', + reference: 'REF-EXP-003', + benefeciaryName: 'Agus Prasetyo', + benefeciaryCompany: 'UD Makmur', + status: 'Lunas', + balanceDue: 0, + total: 4200000 + }, + { + id: 4, + date: '2025-01-08', + number: 'EXP/2025/004', + reference: 'REF-EXP-004', + benefeciaryName: 'Rina Wulandari', + benefeciaryCompany: 'PT Sejahtera Bersama', + status: 'Belum Dibayar', + balanceDue: 3200000, + total: 3200000 + }, + { + id: 5, + date: '2025-01-09', + number: 'EXP/2025/005', + reference: 'REF-EXP-005', + benefeciaryName: 'Dedi Kurniawan', + benefeciaryCompany: 'CV Bintang Terang', + status: 'Dibayar Sebagian', + balanceDue: 1500000, + total: 7000000 + }, + { + id: 6, + date: '2025-01-10', + number: 'EXP/2025/006', + reference: 'REF-EXP-006', + benefeciaryName: 'Sri Lestari', + benefeciaryCompany: 'PT Barokah Jaya', + status: 'Lunas', + balanceDue: 0, + total: 2800000 + }, + { + id: 7, + date: '2025-01-11', + number: 'EXP/2025/007', + reference: 'REF-EXP-007', + benefeciaryName: 'Joko Widodo', + benefeciaryCompany: 'UD Sumber Rejeki', + status: 'Belum Dibayar', + balanceDue: 8000000, + total: 8000000 + }, + { + id: 8, + date: '2025-01-12', + number: 'EXP/2025/008', + reference: 'REF-EXP-008', + benefeciaryName: 'Maya Kartika', + benefeciaryCompany: 'PT Bumi Lestari', + status: 'Dibayar Sebagian', + balanceDue: 2000000, + total: 9000000 + }, + { + id: 9, + date: '2025-01-13', + number: 'EXP/2025/009', + reference: 'REF-EXP-009', + benefeciaryName: 'Eko Yulianto', + benefeciaryCompany: 'CV Cahaya Baru', + status: 'Lunas', + balanceDue: 0, + total: 3500000 + }, + { + id: 10, + date: '2025-01-14', + number: 'EXP/2025/010', + reference: 'REF-EXP-010', + benefeciaryName: 'Nina Rahmawati', + benefeciaryCompany: 'PT Gemilang', + status: 'Belum Dibayar', + balanceDue: 4700000, + total: 4700000 + }, + { + id: 11, + date: '2025-01-15', + number: 'EXP/2025/011', + reference: 'REF-EXP-011', + benefeciaryName: 'Andi Saputra', + benefeciaryCompany: 'CV Harmoni', + status: 'Dibayar Sebagian', + balanceDue: 1200000, + total: 6000000 + }, + { + id: 12, + date: '2025-01-16', + number: 'EXP/2025/012', + reference: 'REF-EXP-012', + benefeciaryName: 'Yuni Astuti', + benefeciaryCompany: 'PT Surya Abadi', + status: 'Lunas', + balanceDue: 0, + total: 5200000 + }, + { + id: 13, + date: '2025-01-17', + number: 'EXP/2025/013', + reference: 'REF-EXP-013', + benefeciaryName: 'Ridwan Hidayat', + benefeciaryCompany: 'UD Berkah', + status: 'Belum Dibayar', + balanceDue: 2900000, + total: 2900000 + }, + { + id: 14, + date: '2025-01-18', + number: 'EXP/2025/014', + reference: 'REF-EXP-014', + benefeciaryName: 'Ratna Sari', + benefeciaryCompany: 'PT Amanah Sentosa', + status: 'Dibayar Sebagian', + balanceDue: 1000000, + total: 4000000 + }, + { + id: 15, + date: '2025-01-19', + number: 'EXP/2025/015', + reference: 'REF-EXP-015', + benefeciaryName: 'Hendra Gunawan', + benefeciaryCompany: 'CV Murni', + status: 'Lunas', + balanceDue: 0, + total: 2500000 + }, + { + id: 16, + date: '2025-01-20', + number: 'EXP/2025/016', + reference: 'REF-EXP-016', + benefeciaryName: 'Mega Putri', + benefeciaryCompany: 'PT Citra Mandiri', + status: 'Belum Dibayar', + balanceDue: 6100000, + total: 6100000 + }, + { + id: 17, + date: '2025-01-21', + number: 'EXP/2025/017', + reference: 'REF-EXP-017', + benefeciaryName: 'Bayu Saputra', + benefeciaryCompany: 'UD Lancar Jaya', + status: 'Dibayar Sebagian', + balanceDue: 1700000, + total: 5000000 + }, + { + id: 18, + date: '2025-01-22', + number: 'EXP/2025/018', + reference: 'REF-EXP-018', + benefeciaryName: 'Dian Anggraini', + benefeciaryCompany: 'CV Sumber Cahaya', + status: 'Lunas', + balanceDue: 0, + total: 3300000 + }, + { + id: 19, + date: '2025-01-23', + number: 'EXP/2025/019', + reference: 'REF-EXP-019', + benefeciaryName: 'Rizky Aditya', + benefeciaryCompany: 'PT Mandiri Abadi', + status: 'Belum Dibayar', + balanceDue: 7000000, + total: 7000000 + }, + { + id: 20, + date: '2025-01-24', + number: 'EXP/2025/020', + reference: 'REF-EXP-020', + benefeciaryName: 'Lina Marlina', + benefeciaryCompany: 'CV Anugerah', + status: 'Dibayar Sebagian', + balanceDue: 2000000, + total: 6500000 + } +] diff --git a/src/services/api.ts b/src/services/api.ts index 7dc7a13..beec3a6 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -33,6 +33,8 @@ api.interceptors.response.use( const currentPath = window.location.pathname if (status === 401 && !currentPath.endsWith('/login')) { + localStorage.removeItem('user') + localStorage.removeItem('authToken') window.location.href = '/login' } @@ -47,4 +49,3 @@ api.interceptors.response.use( return Promise.reject(error) } ) - diff --git a/src/types/apps/expenseType.ts b/src/types/apps/expenseType.ts new file mode 100644 index 0000000..0c7ef2f --- /dev/null +++ b/src/types/apps/expenseType.ts @@ -0,0 +1,11 @@ +export type ExpenseType = { + id: number + date: string + number: string + reference: string + benefeciaryName: string + benefeciaryCompany: string + status: string + balanceDue: number + total: number +} diff --git a/src/views/apps/expense/list/ExpenseListCard.tsx b/src/views/apps/expense/list/ExpenseListCard.tsx new file mode 100644 index 0000000..e828dcc --- /dev/null +++ b/src/views/apps/expense/list/ExpenseListCard.tsx @@ -0,0 +1,51 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Types Imports +import type { CardStatsHorizontalWithAvatarProps } from '@/types/pages/widgetTypes' + +// Component Imports +import CardStatsHorizontalWithAvatar from '@components/card-statistics/HorizontalWithAvatar' + +const data: CardStatsHorizontalWithAvatarProps[] = [ + { + stats: '10.495.100', + title: 'Bulan Ini', + avatarIcon: 'tabler-calendar-month', + avatarColor: 'warning' + }, + { + stats: '25.868.800', + title: '30 Hari Lalu', + avatarIcon: 'tabler-calendar-time', + avatarColor: 'error' + }, + { + stats: '17.903.400', + title: 'Belum Dibayar', + avatarIcon: 'tabler-clock-exclamation', + avatarColor: 'warning' + }, + { + stats: '13.467.200', + title: 'Jatuh Tempo', + avatarIcon: 'tabler-calendar-due', + avatarColor: 'success' + } +] + +const ExpenseListCard = () => { + return ( + data && ( + + {data.map((item, index) => ( + + + + ))} + + ) + ) +} + +export default ExpenseListCard diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx new file mode 100644 index 0000000..54a2495 --- /dev/null +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -0,0 +1,519 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import { ExpenseType } from '@/types/apps/expenseType' +import { expenseData } from '@/data/dummy/expense' +import StatusFilterTabs from '@/components/StatusFilterTab' +import DateRangePicker from '@/components/RangeDatePicker' + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type ExpenseTypeWithAction = ExpenseType & { + actions?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Status color mapping for Expense +const getStatusColor = (status: string) => { + switch (status) { + case 'Belum Dibayar': + return 'error' + case 'Dibayar Sebagian': + return 'warning' + case 'Lunas': + return 'success' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const ExpenseListTable = () => { + const dispatch = useDispatch() + + // States + const [addExpenseOpen, setAddExpenseOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [expenseId, setExpenseId] = useState('') + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(expenseData) + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + // Hooks + const { lang: locale } = useParams() + + // Filter data based on search and status + useEffect(() => { + let filtered = expenseData + + // Filter by search + if (search) { + filtered = filtered.filter( + expense => + expense.number.toLowerCase().includes(search.toLowerCase()) || + expense.benefeciaryName.toLowerCase().includes(search.toLowerCase()) || + expense.benefeciaryCompany.toLowerCase().includes(search.toLowerCase()) || + expense.status.toLowerCase().includes(search.toLowerCase()) || + expense.reference.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by status + if (statusFilter !== 'Semua') { + filtered = filtered.filter(expense => expense.status === statusFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, statusFilter]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + // Calculate subtotal and total from filtered data + const subtotalBalanceDue = useMemo(() => { + return filteredData.reduce((sum, expense) => sum + expense.balanceDue, 0) + }, [filteredData]) + + const subtotalTotal = useMemo(() => { + return filteredData.reduce((sum, expense) => sum + expense.total, 0) + }, [filteredData]) + + // For demonstration, adding tax/additional fees to show difference between subtotal and total + const taxAmount = subtotalTotal * 0.1 // 10% tax example + const finalTotal = subtotalTotal + taxAmount + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleExpenseClick = (expenseId: string) => { + console.log('Navigasi ke detail Expense:', expenseId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('number', { + header: 'Nomor Expense', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('benefeciaryName', { + header: 'Beneficiary', + cell: ({ row }) => ( +
+ + {row.original.benefeciaryName} + + + {row.original.benefeciaryCompany} + +
+ ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal', + cell: ({ row }) => {row.original.date} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('balanceDue', { + header: 'Sisa Tagihan', + cell: ({ row }) => ( + 0 ? 'text-red-600' : 'text-green-600'}`}> + {formatCurrency(row.original.balanceDue)} + + ) + }), + columnHelper.accessor('total', { + header: 'Total', + cell: ({ row }) => {formatCurrency(row.original.total)} + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as ExpenseType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs and Range Date */} +
+
+ + +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Expense' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + {/* Subtotal Row */} + + + + + + + + + + + + {/* Total Row */} + + + + + + + + + + + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + Subtotal + + + + {formatCurrency(subtotalBalanceDue)} + + + + {formatCurrency(subtotalTotal)} + +
+ + Total + + + + {formatCurrency(subtotalBalanceDue + taxAmount)} + + + + {formatCurrency(finalTotal)} + +
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default ExpenseListTable diff --git a/src/views/apps/expense/list/index.tsx b/src/views/apps/expense/list/index.tsx new file mode 100644 index 0000000..6d14bf5 --- /dev/null +++ b/src/views/apps/expense/list/index.tsx @@ -0,0 +1,23 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' +import ExpenseListTable from './ExpenseListTable' +import ExpenseListCard from './ExpenseListCard' + +// Type Imports + +// Component Imports + +const ExpenseList = () => { + return ( + + + + + + + + + ) +} + +export default ExpenseList From 420df714525f9c19b6a0206002d6e9bf876e0b2a Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 14:31:16 +0700 Subject: [PATCH 21/42] Sales Bill List table Update --- .../apps/expense/list/ExpenseListTable.tsx | 1 - .../sales-bill/list/SalesBillListTable.tsx | 110 ++++++++++++++---- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx index 54a2495..dde2737 100644 --- a/src/views/apps/expense/list/ExpenseListTable.tsx +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -188,7 +188,6 @@ const ExpenseListTable = () => { return filteredData.reduce((sum, expense) => sum + expense.total, 0) }, [filteredData]) - // For demonstration, adding tax/additional fees to show difference between subtotal and total const taxAmount = subtotalTotal * 0.1 // 10% tax example const finalTotal = subtotalTotal + taxAmount diff --git a/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx index 6245605..3c98f01 100644 --- a/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx +++ b/src/views/apps/sales/sales-bill/list/SalesBillListTable.tsx @@ -42,6 +42,8 @@ import Loading from '@/components/layout/shared/Loading' import { getLocalizedUrl } from '@/utils/i18n' import { SalesBillType } from '@/types/apps/salesTypes' import { salesBillData } from '@/data/dummy/sales' +import DateRangePicker from '@/components/RangeDatePicker' +import StatusFilterTabs from '@/components/StatusFilterTab' declare module '@tanstack/table-core' { interface FilterFns { @@ -144,6 +146,8 @@ const SalesBillListTable = () => { const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('Semua') const [filteredData, setFilteredData] = useState(salesBillData) + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) // Hooks const { lang: locale } = useParams() @@ -178,6 +182,17 @@ const SalesBillListTable = () => { return filteredData.slice(startIndex, startIndex + pageSize) }, [filteredData, currentPage, pageSize]) + const subtotalremainingBill = useMemo(() => { + return filteredData.reduce((sum, expense) => sum + expense.remainingBill, 0) + }, [filteredData]) + + const subtotalTotal = useMemo(() => { + return filteredData.reduce((sum, expense) => sum + expense.total, 0) + }, [filteredData]) + + const taxAmount = subtotalTotal * 0.1 // 10% tax example + const finalTotal = subtotalTotal + taxAmount + const handlePageChange = useCallback((event: unknown, newPage: number) => { setCurrentPage(newPage) }, []) @@ -323,30 +338,29 @@ const SalesBillListTable = () => { return ( <> - {/* Filter Status Tabs */} + {/* Filter Status Tabs and Range Date */}
-
- {['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas', 'Void', 'Retur'].map(status => ( - - ))} +
+ +
@@ -435,6 +449,56 @@ const SalesBillListTable = () => { ) })} + + {/* Subtotal Row */} + + + + Subtotal + + + + + + + + + + + {formatCurrency(subtotalremainingBill)} + + + + + {formatCurrency(subtotalTotal)} + + + + + {/* Total Row */} + + + + Total + + + + + + + + + + + {formatCurrency(subtotalremainingBill + taxAmount)} + + + + + {formatCurrency(finalTotal)} + + + )} From afa4cfad0de8d1f64bfa20bd91eeedb35b8bdcd5 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 15:45:34 +0700 Subject: [PATCH 22/42] Expense Add --- .../(private)/apps/expense/add/page.tsx | 18 + src/data/dummy/expense.ts | 2 +- src/types/apps/expenseType.ts | 11 - src/types/apps/expenseTypes.ts | 68 ++++ .../apps/expense/add/ExpenseAddAccount.tsx | 137 +++++++ .../apps/expense/add/ExpenseAddBasicInfo.tsx | 166 +++++++++ src/views/apps/expense/add/ExpenseAddForm.tsx | 149 ++++++++ .../apps/expense/add/ExpenseAddHeader.tsx | 19 + .../apps/expense/add/ExpenseAddSummary.tsx | 198 ++++++++++ .../apps/expense/list/ExpenseListTable.tsx | 4 +- .../PurchaseIngredientsTable.tsx | 340 ++++++++---------- .../purchase-form/PurchaseSummary.tsx | 117 +++--- 12 files changed, 980 insertions(+), 249 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx delete mode 100644 src/types/apps/expenseType.ts create mode 100644 src/types/apps/expenseTypes.ts create mode 100644 src/views/apps/expense/add/ExpenseAddAccount.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddBasicInfo.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddForm.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddHeader.tsx create mode 100644 src/views/apps/expense/add/ExpenseAddSummary.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx new file mode 100644 index 0000000..65f30df --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/expense/add/page.tsx @@ -0,0 +1,18 @@ +import ExpenseAddForm from '@/views/apps/expense/add/ExpenseAddForm' +import ExpenseAddHeader from '@/views/apps/expense/add/ExpenseAddHeader' +import Grid from '@mui/material/Grid2' + +const ExpenseAddPage = () => { + return ( + + + + + + + + + ) +} + +export default ExpenseAddPage diff --git a/src/data/dummy/expense.ts b/src/data/dummy/expense.ts index 85c5ec1..7d87601 100644 --- a/src/data/dummy/expense.ts +++ b/src/data/dummy/expense.ts @@ -1,4 +1,4 @@ -import { ExpenseType } from '@/types/apps/expenseType' +import { ExpenseType } from '@/types/apps/expenseTypes' export const expenseData: ExpenseType[] = [ { diff --git a/src/types/apps/expenseType.ts b/src/types/apps/expenseType.ts deleted file mode 100644 index 0c7ef2f..0000000 --- a/src/types/apps/expenseType.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type ExpenseType = { - id: number - date: string - number: string - reference: string - benefeciaryName: string - benefeciaryCompany: string - status: string - balanceDue: number - total: number -} diff --git a/src/types/apps/expenseTypes.ts b/src/types/apps/expenseTypes.ts new file mode 100644 index 0000000..811cf6e --- /dev/null +++ b/src/types/apps/expenseTypes.ts @@ -0,0 +1,68 @@ +export type ExpenseType = { + id: number + date: string + number: string + reference: string + benefeciaryName: string + benefeciaryCompany: string + status: string + balanceDue: number + total: number +} + +export interface ExpenseRecipient { + id: string + name: string +} + +export interface ExpenseAccount { + id: string + name: string + code: string +} + +export interface ExpenseTax { + id: string + name: string + rate: number +} + +export interface ExpenseTag { + id: string + name: string + color: string +} + +export interface ExpenseItem { + id: number + account: ExpenseAccount | null + description: string + tax: ExpenseTax | null + total: number +} + +export interface ExpenseFormData { + // Header fields + paidFrom: string // "Dibayar Dari" + payLater: boolean // "Bayar Nanti" toggle + recipient: ExpenseRecipient | null // "Penerima" + transactionDate: string // "Tgl. Transaksi" + + // Reference fields + number: string // "Nomor" + reference: string // "Referensi" + tag: ExpenseTag | null // "Tag" + includeTax: boolean // "Harga termasuk pajak" + + // Expense items + expenseItems: ExpenseItem[] + + // Totals + subtotal: number + total: number + + // Optional sections + showMessage: boolean + showAttachment: boolean + message: string +} diff --git a/src/views/apps/expense/add/ExpenseAddAccount.tsx b/src/views/apps/expense/add/ExpenseAddAccount.tsx new file mode 100644 index 0000000..19bef61 --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddAccount.tsx @@ -0,0 +1,137 @@ +'use client' + +import React from 'react' +import { + Button, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + MenuItem +} from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData, ExpenseItem, ExpenseAccount, ExpenseTax } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@core/components/mui/Autocomplete' + +interface ExpenseAddAccountProps { + formData: ExpenseFormData + mockAccounts: ExpenseAccount[] + mockTaxes: ExpenseTax[] + onExpenseItemChange: (index: number, field: keyof ExpenseItem, value: any) => void + onAddExpenseItem: () => void + onRemoveExpenseItem: (index: number) => void +} + +const ExpenseAddAccount: React.FC = ({ + formData, + mockAccounts, + mockTaxes, + onExpenseItemChange, + onAddExpenseItem, + onRemoveExpenseItem +}) => { + return ( + + + + + + Akun Biaya + Deskripsi + Pajak + Total + + + + + {formData.expenseItems.map((item, index) => ( + + + `${option.code} ${option.name}`} + value={item.account} + onChange={(_, value) => onExpenseItemChange(index, 'account', value)} + renderInput={params => } + /> + + + onExpenseItemChange(index, 'description', e.target.value)} + /> + + + { + const tax = mockTaxes.find(t => t.id === e.target.value) + onExpenseItemChange(index, 'tax', tax || null) + }} + SelectProps={{ + displayEmpty: true + }} + > + ... + {mockTaxes.map(tax => ( + + {tax.name} + + ))} + + + + onExpenseItemChange(index, 'total', parseFloat(e.target.value) || 0)} + sx={{ + '& .MuiInputBase-input': { + textAlign: 'right' + } + }} + /> + + + onRemoveExpenseItem(index)} + disabled={formData.expenseItems.length === 1} + > + + + + + ))} + +
+
+ + +
+ ) +} + +export default ExpenseAddAccount diff --git a/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx b/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx new file mode 100644 index 0000000..b268fed --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddBasicInfo.tsx @@ -0,0 +1,166 @@ +'use client' + +import React from 'react' +import { Switch, FormControlLabel, Box } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData, ExpenseAccount, ExpenseRecipient, ExpenseTag } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@core/components/mui/Autocomplete' + +interface ExpenseAddBasicInfoProps { + formData: ExpenseFormData + mockAccounts: ExpenseAccount[] + mockRecipients: ExpenseRecipient[] + mockTags: ExpenseTag[] + onInputChange: (field: keyof ExpenseFormData, value: any) => void +} + +const ExpenseAddBasicInfo: React.FC = ({ + formData, + mockAccounts, + mockRecipients, + mockTags, + onInputChange +}) => { + return ( + <> + {/* Row 1: Dibayar Dari (6) + Bayar Nanti (6) */} + + `${option.code} ${option.name}`} + value={mockAccounts.find(acc => `${acc.code} ${acc.name}` === formData.paidFrom) || null} + onChange={(_, value) => onInputChange('paidFrom', value ? `${value.code} ${value.name}` : '')} + renderInput={params => } + /> + + + + onInputChange('payLater', e.target.checked)} + size='small' + /> + } + label='Bayar Nanti' + /> + + + + {/* Row 2: Penerima (6) */} + + option.name} + value={formData.recipient} + onChange={(_, value) => onInputChange('recipient', value)} + renderInput={params => } + /> + + + {/* Row 3: Tgl. Transaksi (6) */} + + { + const date = new Date(e.target.value) + const formattedDate = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear()}` + onInputChange('transactionDate', formattedDate) + }} + slotProps={{ + input: { + endAdornment: + } + }} + /> + + + {/* Row 4: Nomor, Referensi, Tag */} + + + + + Nomor + + + } + value={formData.number} + onChange={e => onInputChange('number', e.target.value)} + /> + + + + Referensi + + + } + placeholder='Referensi' + value={formData.reference} + onChange={e => onInputChange('reference', e.target.value)} + /> + + + + Tag + + + } + placeholder='Pilih Tag' + value={formData.tag?.name || ''} + onChange={e => { + const tag = mockTags.find(t => t.name === e.target.value) + onInputChange('tag', tag || null) + }} + /> + + + + + {/* Row 5: Harga termasuk pajak */} + + + onInputChange('includeTax', e.target.checked)} + size='small' + /> + } + label='Harga termasuk pajak' + /> + + + + ) +} + +export default ExpenseAddBasicInfo diff --git a/src/views/apps/expense/add/ExpenseAddForm.tsx b/src/views/apps/expense/add/ExpenseAddForm.tsx new file mode 100644 index 0000000..60425d0 --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddForm.tsx @@ -0,0 +1,149 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { + ExpenseFormData, + ExpenseItem, + ExpenseAccount, + ExpenseTax, + ExpenseTag, + ExpenseRecipient +} from '@/types/apps/expenseTypes' +import ExpenseAddBasicInfo from './ExpenseAddBasicInfo' +import ExpenseAddAccount from './ExpenseAddAccount' +import ExpenseAddSummary from './ExpenseAddSummary' + +// Mock data +const mockAccounts: ExpenseAccount[] = [ + { id: '1', name: 'Kas', code: '1-10001' }, + { id: '2', name: 'Bank BCA', code: '1-10002' }, + { id: '3', name: 'Bank Mandiri', code: '1-10003' } +] + +const mockRecipients: ExpenseRecipient[] = [ + { id: '1', name: 'PT ABC Company' }, + { id: '2', name: 'CV XYZ Trading' }, + { id: '3', name: 'John Doe' }, + { id: '4', name: 'Jane Smith' } +] + +const mockTaxes: ExpenseTax[] = [ + { id: '1', name: 'PPN 11%', rate: 11 }, + { id: '2', name: 'PPh 23', rate: 2 }, + { id: '3', name: 'Bebas Pajak', rate: 0 } +] + +const mockTags: ExpenseTag[] = [ + { id: '1', name: 'Operasional', color: '#2196F3' }, + { id: '2', name: 'Marketing', color: '#4CAF50' }, + { id: '3', name: 'IT', color: '#FF9800' } +] + +const ExpenseAddForm: React.FC = () => { + const [formData, setFormData] = useState({ + paidFrom: '1-10001 Kas', + payLater: false, + recipient: null, + transactionDate: '13/09/2025', + number: 'EXP/00042', + reference: '', + tag: null, + includeTax: false, + expenseItems: [ + { + id: 1, + account: null, + description: '', + tax: null, + total: 0 + } + ], + subtotal: 0, + total: 0, + showMessage: false, + showAttachment: false, + message: '' + }) + + const handleInputChange = (field: keyof ExpenseFormData, value: any): void => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + const handleExpenseItemChange = (index: number, field: keyof ExpenseItem, value: any): void => { + setFormData(prev => { + const newItems = [...prev.expenseItems] + newItems[index] = { ...newItems[index], [field]: value } + + const subtotal = newItems.reduce((sum, item) => sum + item.total, 0) + const total = subtotal + + return { + ...prev, + expenseItems: newItems, + subtotal, + total + } + }) + } + + const addExpenseItem = (): void => { + const newItem: ExpenseItem = { + id: Date.now(), + account: null, + description: '', + tax: null, + total: 0 + } + setFormData(prev => ({ + ...prev, + expenseItems: [...prev.expenseItems, newItem] + })) + } + + const removeExpenseItem = (index: number): void => { + if (formData.expenseItems.length > 1) { + setFormData(prev => ({ + ...prev, + expenseItems: prev.expenseItems.filter((_, i) => i !== index) + })) + } + } + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + + + + + + + + + + ) +} + +export default ExpenseAddForm diff --git a/src/views/apps/expense/add/ExpenseAddHeader.tsx b/src/views/apps/expense/add/ExpenseAddHeader.tsx new file mode 100644 index 0000000..5cbd1ca --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const ExpenseAddHeader = () => { + return ( +
+
+ + Tambah Biaya + +
+
+ ) +} + +export default ExpenseAddHeader diff --git a/src/views/apps/expense/add/ExpenseAddSummary.tsx b/src/views/apps/expense/add/ExpenseAddSummary.tsx new file mode 100644 index 0000000..63f071e --- /dev/null +++ b/src/views/apps/expense/add/ExpenseAddSummary.tsx @@ -0,0 +1,198 @@ +'use client' + +import React from 'react' +import { Button, Accordion, AccordionSummary, AccordionDetails, Typography, Box } from '@mui/material' +import Grid from '@mui/material/Grid2' +import { ExpenseFormData } from '@/types/apps/expenseTypes' +import CustomTextField from '@core/components/mui/TextField' +import ImageUpload from '@/components/ImageUpload' + +interface ExpenseAddSummaryProps { + formData: ExpenseFormData + onInputChange: (field: keyof ExpenseFormData, value: any) => void + formatCurrency: (amount: number) => string +} + +const ExpenseAddSummary: React.FC = ({ formData, onInputChange, formatCurrency }) => { + const handleUpload = async (file: File): Promise => { + // Simulate upload + return new Promise(resolve => { + setTimeout(() => { + resolve(URL.createObjectURL(file)) + }, 1000) + }) + } + + return ( + + + {/* Left Side - Pesan and Attachment */} + + {/* Pesan Section */} + + + {formData.showMessage && ( + + ) => onInputChange('message', e.target.value)} + /> + + )} + + + {/* Attachment Section */} + + + {formData.showAttachment && ( + + )} + + + + {/* Right Side - Totals */} + + + {/* Sub Total */} + + + Sub Total + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(formData.subtotal || 0)} + + + + {/* Additional Options */} + + {/* Total */} + + + Total + + + RP. 120.000 + + + + {/* Save Button */} + + + + + + ) +} + +export default ExpenseAddSummary diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx index dde2737..83d527f 100644 --- a/src/views/apps/expense/list/ExpenseListTable.tsx +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -40,7 +40,7 @@ import { useDispatch } from 'react-redux' import TablePaginationComponent from '@/components/TablePaginationComponent' import Loading from '@/components/layout/shared/Loading' import { getLocalizedUrl } from '@/utils/i18n' -import { ExpenseType } from '@/types/apps/expenseType' +import { ExpenseType } from '@/types/apps/expenseTypes' import { expenseData } from '@/data/dummy/expense' import StatusFilterTabs from '@/components/StatusFilterTab' import DateRangePicker from '@/components/RangeDatePicker' @@ -387,7 +387,7 @@ const ExpenseListTable = () => { component={Link} className='max-sm:is-full is-auto' startIcon={} - href={getLocalizedUrl('/apps/expenses/add', locale as Locale)} + href={getLocalizedUrl('/apps/expense/add', locale as Locale)} > Tambah diff --git a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx index d319568..8514a0a 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseIngredientsTable.tsx @@ -1,7 +1,18 @@ 'use client' import React from 'react' -import { Button, Typography } from '@mui/material' +import { + Button, + Typography, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper +} from '@mui/material' import Grid from '@mui/material/Grid2' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' import CustomTextField from '@/@core/components/mui/TextField' @@ -53,195 +64,160 @@ const PurchaseIngredientsTable: React.FC = ({ ] return ( - + Bahan Baku / Ingredients - {/* Table Header */} - - - - Bahan Baku - - - - - Deskripsi - - - - - Kuantitas - - - - - Satuan - - - - - Discount - - - - - Harga - - - - - Pajak - - - - - Waste - - - - - Total - - - - + + + + + Bahan Baku + Deskripsi + Kuantitas + Satuan + Discount + Harga + Pajak + Waste + Total + + + + + {formData.ingredientItems.map((item: IngredientItem, index: number) => ( + + + handleIngredientChange(index, 'ingredient', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'deskripsi', e.target.value) + } + placeholder='Deskripsi' + /> + + + ) => + handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) + } + inputProps={{ min: 1 }} + /> + + + handleIngredientChange(index, 'satuan', newValue)} + renderInput={params => } + /> + + + ) => + handleIngredientChange(index, 'discount', e.target.value) + } + placeholder='0%' + /> + + + ) => { + const value = e.target.value - {/* Ingredient Items */} - {formData.ingredientItems.map((item: IngredientItem, index: number) => ( - - - handleIngredientChange(index, 'ingredient', newValue)} - renderInput={params => ( - - )} - /> - - - ) => - handleIngredientChange(index, 'deskripsi', e.target.value) - } - placeholder='Deskripsi' - /> - - - ) => - handleIngredientChange(index, 'kuantitas', parseInt(e.target.value) || 1) - } - inputProps={{ min: 1 }} - /> - - - handleIngredientChange(index, 'satuan', newValue)} - renderInput={params => } - /> - - - ) => - handleIngredientChange(index, 'discount', e.target.value) - } - placeholder='0%' - /> - - - ) => { - const value = e.target.value + if (value === '') { + handleIngredientChange(index, 'harga', null) + return + } - if (value === '') { - // Jika kosong, set ke null atau undefined, bukan 0 - handleIngredientChange(index, 'harga', null) // atau undefined - return - } - - const numericValue = parseFloat(value) - handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) - }} - inputProps={{ min: 0, step: 'any' }} - placeholder='0' - /> - - - handleIngredientChange(index, 'pajak', newValue)} - renderInput={params => } - /> - - - handleIngredientChange(index, 'waste', newValue)} - renderInput={params => } - /> - - - - - - - - - ))} + const numericValue = parseFloat(value) + handleIngredientChange(index, 'harga', isNaN(numericValue) ? 0 : numericValue) + }} + inputProps={{ min: 0, step: 'any' }} + placeholder='0' + /> + + + handleIngredientChange(index, 'pajak', newValue)} + renderInput={params => } + /> + + + handleIngredientChange(index, 'waste', newValue)} + renderInput={params => } + /> + + + + + + removeIngredientItem(index)} + disabled={formData.ingredientItems.length === 1} + > + + + + + ))} + +
+
{/* Add New Item Button */} - - - - - +
) } diff --git a/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx b/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx index 298686a..3bc71cf 100644 --- a/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx +++ b/src/views/apps/purchase/purchase-form/PurchaseSummary.tsx @@ -464,64 +464,75 @@ const PurchaseSummary: React.FC = ({ formData, handleInput } }} > - - - {/* Dropdown */} - (typeof option === 'string' ? option : option.label)} - value={{ label: '1-10003 Gi...', value: '1-10003' }} - onChange={(_, newValue) => { - // Handle change if needed - }} - renderInput={params => } - sx={{ minWidth: 120 }} - /> + + {formData.showUangMuka && ( + + + {/* Dropdown */} + (typeof option === 'string' ? option : option.label)} + value={{ label: '1-10003 Gi...', value: '1-10003' }} + onChange={(_, newValue) => { + // Handle change if needed + }} + renderInput={params => } + sx={{ minWidth: 120 }} + /> - {/* Amount input */} - ) => - handleInputChange('downPayment', e.target.value) - } - sx={{ width: '80px' }} - inputProps={{ - style: { textAlign: 'center' } - }} - /> + {/* Amount input */} + ) => + handleInputChange('downPayment', e.target.value) + } + sx={{ width: '80px' }} + inputProps={{ + style: { textAlign: 'center' } + }} + /> - {/* Percentage/Fixed toggle */} - { - if (newValue) handleInputChange('downPaymentType', newValue) + {/* Percentage/Fixed toggle */} + { + if (newValue) handleInputChange('downPaymentType', newValue) + }} + size='small' + > + + % + + + Rp + + + + + {/* Right side text */} + - - % - - - Rp - - + Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} + - - {/* Right side text */} - - Uang muka {downPayment > 0 ? downPayment.toLocaleString('id-ID') : '0'} - - + )} {/* Sisa Tagihan */} From 00620026e40f4a3654b95b700c70625fdb8df50c Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 16:55:19 +0700 Subject: [PATCH 23/42] Detail expense --- .../apps/expense/[id]/detail/page.tsx | 18 + .../expense/detail/ExpenseDetailContent.tsx | 22 + .../expense/detail/ExpenseDetailHeader.tsx | 15 + .../detail/ExpenseDetailInformation.tsx | 265 +++++++++++ .../apps/expense/detail/ExpenseDetailLog.tsx | 59 +++ .../detail/ExpenseDetailSendPayment.tsx | 417 ++++++++++++++++++ .../apps/expense/list/ExpenseListTable.tsx | 3 +- 7 files changed, 798 insertions(+), 1 deletion(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx create mode 100644 src/views/apps/expense/detail/ExpenseDetailContent.tsx create mode 100644 src/views/apps/expense/detail/ExpenseDetailHeader.tsx create mode 100644 src/views/apps/expense/detail/ExpenseDetailInformation.tsx create mode 100644 src/views/apps/expense/detail/ExpenseDetailLog.tsx create mode 100644 src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx new file mode 100644 index 0000000..68c6855 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/expense/[id]/detail/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetailContent from '@/views/apps/expense/detail/ExpenseDetailContent' +import ExpenseDetailHeader from '@/views/apps/expense/detail/ExpenseDetailHeader' +import Grid from '@mui/material/Grid2' + +const ExpenseDetailPage = () => { + return ( + + + + + + + + + ) +} + +export default ExpenseDetailPage diff --git a/src/views/apps/expense/detail/ExpenseDetailContent.tsx b/src/views/apps/expense/detail/ExpenseDetailContent.tsx new file mode 100644 index 0000000..accf703 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailContent.tsx @@ -0,0 +1,22 @@ +import Grid from '@mui/material/Grid2' +import ExpenseDetailInformation from './ExpenseDetailInformation' +import ExpenseDetailSendPayment from './ExpenseDetailSendPayment' +import ExpenseDetailLog from './ExpenseDetailLog' + +const ExpenseDetailContent = () => { + return ( + + + + + + + + + + + + ) +} + +export default ExpenseDetailContent diff --git a/src/views/apps/expense/detail/ExpenseDetailHeader.tsx b/src/views/apps/expense/detail/ExpenseDetailHeader.tsx new file mode 100644 index 0000000..f0b5681 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailHeader.tsx @@ -0,0 +1,15 @@ +import { Typography } from '@mui/material' + +const ExpenseDetailHeader = () => { + return ( +
+
+ + Detail Biaya + +
+
+ ) +} + +export default ExpenseDetailHeader diff --git a/src/views/apps/expense/detail/ExpenseDetailInformation.tsx b/src/views/apps/expense/detail/ExpenseDetailInformation.tsx new file mode 100644 index 0000000..fe58ae7 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailInformation.tsx @@ -0,0 +1,265 @@ +import React from 'react' +import { Card, CardHeader, CardContent, Typography, Box, Button, IconButton } from '@mui/material' +import Grid from '@mui/material/Grid2' + +interface Product { + produk: string + deskripsi: string + kuantitas: number + satuan: string + discount: string + harga: number + pajak: string + jumlah: number +} + +interface PurchaseData { + vendor: string + nomor: string + tglTransaksi: string + tglJatuhTempo: string + gudang: string + status: string +} + +const ExpenseDetailInformation: React.FC = () => { + const purchaseData: PurchaseData = { + vendor: 'Bagas Rizki Sihotang S.Farm Widodo', + nomor: 'PI/00053', + tglTransaksi: '08/09/2025', + tglJatuhTempo: '06/10/2025', + gudang: 'Unassigned', + status: 'Belum Dibayar' + } + + const products: Product[] = [ + { + produk: 'Komisi penjualan tim IT', + deskripsi: '6-60002 - Komisi & Fee (PPN)', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 810000, + pajak: 'PPN', + jumlah: 810000 + }, + { + produk: 'Hotel Ibis 3 Malam, Surabaya', + deskripsi: '6-60004 - Perjalanan Dinas - Penjualan (PPN)', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 400000, + pajak: 'PPN', + jumlah: 400000 + }, + { + produk: 'Biaya telp fixed', + deskripsi: '6-60005 - Komunikasi - Penjualan', + kuantitas: 1, + satuan: 'Pcs', + discount: '0%', + harga: 950000, + pajak: 'PPN', + jumlah: 950000 + } + ] + + const subTotal: number = products.reduce((sum, product) => sum + product.jumlah, 0) + const ppn: number = Math.round(subTotal * 0.11) + const total: number = subTotal + ppn + const sisaTagihan: number = total + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + + + + Belum Dibayar + + + + + + + + + + } + /> + + + {/* Purchase Information */} + + + + + Penerima + + + {purchaseData.vendor} + + + + + + Tgl. Transaksi + + {purchaseData.tglTransaksi} + + + + + + + Nomor + + {purchaseData.nomor} + + + + + Tgl. Jatuh Tempo + + {purchaseData.tglJatuhTempo} + + + + + {/* Products List */} + + {products.map((product, index) => ( + + + + {product.produk} + + + {product.deskripsi} + + + + + {formatCurrency(product.jumlah)} + + + + ))} + + + {/* Summary Section */} + + + {/* Empty space for left side */} + + + + + Sub Total + + + {formatCurrency(subTotal)} + + + + + + PPN + + + {formatCurrency(ppn)} + + + + + + Total + + + {formatCurrency(total)} + + + + + + Sisa Tagihan + + + {formatCurrency(sisaTagihan)} + + + + + + + + + ) +} + +export default ExpenseDetailInformation diff --git a/src/views/apps/expense/detail/ExpenseDetailLog.tsx b/src/views/apps/expense/detail/ExpenseDetailLog.tsx new file mode 100644 index 0000000..0ee010a --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailLog.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardHeader, Typography, Box, Link } from '@mui/material' + +interface LogEntry { + id: string + action: string + timestamp: string + user: string +} + +const ExpenseDetailLog: React.FC = () => { + const logEntries: LogEntry[] = [ + { + id: '1', + action: 'Terakhir diubah oleh', + timestamp: '08 Sep 2025 18:26', + user: 'pada' + } + ] + + return ( + + + Pantau log perubahan data + + } + sx={{ pb: 1 }} + /> + + {logEntries.map(entry => ( + + + + + {entry.action} {entry.user} {entry.timestamp} + + + + ))} + + + ) +} + +export default ExpenseDetailLog diff --git a/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx new file mode 100644 index 0000000..8f2ac20 --- /dev/null +++ b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx @@ -0,0 +1,417 @@ +'use client' + +import React, { useState } from 'react' +import { + Card, + CardContent, + Typography, + Button, + Box, + Accordion, + AccordionSummary, + AccordionDetails, + Tooltip, + IconButton, + CardHeader +} from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomTextField from '@/@core/components/mui/TextField' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' + +interface PaymentFormData { + totalDibayar: string + tglTransaksi: string + referensi: string + nomor: string + dibayarDari: string +} + +interface PemotonganItem { + id: string + dipotong: string + persentase: string + nominal: string + tipe: 'persen' | 'rupiah' +} + +const ExpenseDetailSendPayment: React.FC = () => { + const [formData, setFormData] = useState({ + totalDibayar: '849.000', + tglTransaksi: '10/09/2025', + referensi: '', + nomor: 'PP/00025', + dibayarDari: '1-10001 Kas' + }) + + const [expanded, setExpanded] = useState(false) + const [pemotonganItems, setPemotonganItems] = useState([]) + + const dibayarDariOptions = [ + { label: '1-10001 Kas', value: '1-10001 Kas' }, + { label: '1-10002 Bank BCA', value: '1-10002 Bank BCA' }, + { label: '1-10003 Bank Mandiri', value: '1-10003 Bank Mandiri' }, + { label: '1-10004 Petty Cash', value: '1-10004 Petty Cash' } + ] + + const pemotonganOptions = [ + { label: 'PPN 11%', value: 'ppn' }, + { label: 'PPh 21', value: 'pph21' }, + { label: 'PPh 23', value: 'pph23' }, + { label: 'Biaya Admin', value: 'admin' } + ] + + const handleChange = + (field: keyof PaymentFormData) => (event: React.ChangeEvent | any) => { + setFormData(prev => ({ + ...prev, + [field]: event.target.value + })) + } + + const handleDibayarDariChange = (value: { label: string; value: string } | null) => { + setFormData(prev => ({ + ...prev, + dibayarDari: value?.value || '' + })) + } + + const addPemotongan = () => { + const newItem: PemotonganItem = { + id: Date.now().toString(), + dipotong: '', + persentase: '0', + nominal: '', + tipe: 'persen' + } + setPemotonganItems(prev => [...prev, newItem]) + } + + const removePemotongan = (id: string) => { + setPemotonganItems(prev => prev.filter(item => item.id !== id)) + } + + const updatePemotongan = (id: string, field: keyof PemotonganItem, value: string) => { + setPemotonganItems(prev => prev.map(item => (item.id === id ? { ...item, [field]: value } : item))) + } + + const handleAccordionChange = () => { + setExpanded(!expanded) + } + + const calculatePemotongan = (item: PemotonganItem): number => { + const totalDibayar = parseInt(formData.totalDibayar.replace(/\D/g, '')) || 0 + const nilai = parseFloat(item.persentase) || 0 + + if (item.tipe === 'persen') { + return (totalDibayar * nilai) / 100 + } else { + return nilai + } + } + + const formatCurrency = (amount: string | number): string => { + const numAmount = typeof amount === 'string' ? parseInt(amount.replace(/\D/g, '')) : amount + return new Intl.NumberFormat('id-ID').format(numAmount) + } + + return ( + + + + Kirim Pembayaran + + + } + /> + + + {/* Left Column */} + + {/* Total Dibayar */} + + + * Total Dibayar + + } + value={formData.totalDibayar} + onChange={handleChange('totalDibayar')} + sx={{ + '& .MuiInputBase-root': { + textAlign: 'right' + } + }} + /> + + + {/* Tgl. Transaksi */} + + + * Tgl. Transaksi + + } + type='date' + value={formData.tglTransaksi.split('/').reverse().join('-')} + onChange={handleChange('tglTransaksi')} + slotProps={{ + input: { + endAdornment: + } + }} + /> + + + {/* Referensi */} + + + + Referensi + + + + + + + + + + + {/* Attachment Accordion */} + + + } + sx={{ + backgroundColor: '#f8f9fa', + borderRadius: '8px', + minHeight: '48px', + '& .MuiAccordionSummary-content': { + margin: '12px 0' + } + }} + > + + Attachment + + + + + Drag and drop files here or click to upload + + + + + + {/* Right Column */} + + {/* Nomor */} + + + + Nomor + + + + + + + + + + + {/* Dibayar Dari */} + + + * Dibayar Dari + + option.label || ''} + value={dibayarDariOptions.find(option => option.value === formData.dibayarDari) || null} + onChange={(_, value: { label: string; value: string } | null) => handleDibayarDariChange(value)} + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + {/* Empty space to match Referensi height */} + {/* Empty space */} + + {/* Pemotongan Button - aligned with Attachment */} + + + + + + + {/* Pemotongan Items */} + {pemotonganItems.length > 0 && ( + + {pemotonganItems.map((item, index) => ( + + + + removePemotongan(item.id)} + sx={{ + backgroundColor: '#fff', + border: '1px solid #f44336', + '&:hover': { backgroundColor: '#ffebee' } + }} + > + + + + + + option.label || ''} + value={pemotonganOptions.find(option => option.value === item.dipotong) || null} + onChange={(_, value: { label: string; value: string } | null) => + updatePemotongan(item.id, 'dipotong', value?.value || '') + } + renderInput={(params: any) => } + noOptionsText='Tidak ada pilihan' + /> + + + + updatePemotongan(item.id, 'persentase', e.target.value)} + placeholder='0' + sx={{ + '& .MuiInputBase-root': { + textAlign: 'center' + } + }} + /> + + + + + + + + + + + + + {formatCurrency(calculatePemotongan(item))} + + + + + + ))} + + )} + + {/* Bottom Section */} + + + {/* Empty space */} + + + + Total + + + {formatCurrency(formData.totalDibayar)} + + + + + + + + + + ) +} + +export default ExpenseDetailSendPayment diff --git a/src/views/apps/expense/list/ExpenseListTable.tsx b/src/views/apps/expense/list/ExpenseListTable.tsx index 83d527f..16a87e4 100644 --- a/src/views/apps/expense/list/ExpenseListTable.tsx +++ b/src/views/apps/expense/list/ExpenseListTable.tsx @@ -244,7 +244,8 @@ const ExpenseListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' - onClick={() => handleExpenseClick(row.original.id.toString())} + component={Link} + href={getLocalizedUrl(`/apps/expense/${row.original.id}/detail`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500, From 66589abdd4f1b72510e2530129eb7202a956e704 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 17:41:07 +0700 Subject: [PATCH 24/42] Cash Bank List --- .../(private)/apps/cash-bank/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 8 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/views/apps/cash-bank/CashBankCard.tsx | 263 ++++++++++++++ src/views/apps/cash-bank/CashBankList.tsx | 326 ++++++++++++++++++ 6 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/cash-bank/page.tsx create mode 100644 src/views/apps/cash-bank/CashBankCard.tsx create mode 100644 src/views/apps/cash-bank/CashBankList.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/page.tsx new file mode 100644 index 0000000..b63c066 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/page.tsx @@ -0,0 +1,7 @@ +import CashBankList from '@/views/apps/cash-bank/CashBankList' + +const CashBankPage = () => { + return +} + +export default CashBankPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 007da5d..e963920 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -116,6 +116,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { }> {dictionary['navigation'].list} + } + exactMatch={false} + activeUrl='/apps/cash-bank' + > + {dictionary['navigation'].cash_and_bank} + }> {dictionary['navigation'].list} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index c9ab714..df083d2 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -123,6 +123,7 @@ "deliveries": "Deliveries", "sales_orders": "Orders", "quotes": "Quotes", - "expenses": "Expenses" + "expenses": "Expenses", + "cash_and_bank": "Cash & Bank" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 5c4742b..53ddf6d 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -123,6 +123,7 @@ "deliveries": "Pengiriman", "sales_orders": "Pemesanan", "quotes": "Penawaran", - "expenses": "Biaya" + "expenses": "Biaya", + "cash_and_bank": "Kas & Bank" } } diff --git a/src/views/apps/cash-bank/CashBankCard.tsx b/src/views/apps/cash-bank/CashBankCard.tsx new file mode 100644 index 0000000..94f1b10 --- /dev/null +++ b/src/views/apps/cash-bank/CashBankCard.tsx @@ -0,0 +1,263 @@ +'use client' + +// Next Imports +import dynamic from 'next/dynamic' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' +import Button from '@mui/material/Button' + +// Third-party Imports +import type { ApexOptions } from 'apexcharts' + +// Styled Component Imports +const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) + +// Types +interface Balance { + amount: string | number + label: string +} + +interface ChartData { + name: string + data: number[] +} + +interface CashBankCardProps { + title: string + accountNumber: string + balances: Balance[] + chartData: ChartData[] + categories: string[] + buttonText?: string + buttonIcon?: string + onButtonClick?: () => void + chartColor?: string + height?: number + currency?: 'IDR' | 'USD' | 'EUR' + showButton?: boolean + maxValue?: number + enableHover?: boolean +} + +const CashBankCard = ({ + title, + accountNumber, + balances, + chartData, + categories, + onButtonClick, + chartColor = '#ff6b9d', + height = 300, + currency = 'IDR', + showButton = true, + maxValue, + enableHover = true +}: CashBankCardProps) => { + // Vars + const divider = 'var(--mui-palette-divider)' + const disabledText = 'var(--mui-palette-text-disabled)' + const primaryText = 'var(--mui-palette-text-primary)' + + // Auto calculate maxValue if not provided + const calculatedMaxValue = maxValue || Math.max(...chartData.flatMap(series => series.data)) * 1.2 + + // Currency formatter + const formatCurrency = (value: number) => { + const formatters = { + IDR: new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }), + USD: new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0 + }), + EUR: new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 0 + }) + } + return formatters[currency].format(value) + } + + // Format balance display + const formatBalance = (amount: string | number) => { + if (typeof amount === 'string') return amount + return new Intl.NumberFormat('id-ID').format(amount) + } + + // Y-axis label formatter based on currency + const formatYAxisLabel = (val: number) => { + if (currency === 'IDR') { + return (val / 1000000).toFixed(0) + '.000.000' + } + return (val / 1000).toFixed(0) + 'K' + } + + const options: ApexOptions = { + chart: { + height: height, + type: 'area', + parentHeightOffset: 0, + zoom: { enabled: false }, + toolbar: { show: false } + }, + colors: [chartColor], + fill: { + type: 'gradient', + gradient: { + shade: 'light', + type: 'vertical', + shadeIntensity: 0.4, + gradientToColors: [chartColor], + inverseColors: false, + opacityFrom: 0.8, + opacityTo: 0.1, + stops: [0, 100] + } + }, + stroke: { + curve: 'smooth', + width: 3 + }, + dataLabels: { enabled: false }, + grid: { + show: true, + borderColor: divider, + strokeDashArray: 3, + padding: { + top: -10, + bottom: -10, + left: 20, + right: 20 + }, + xaxis: { + lines: { show: true } + }, + yaxis: { + lines: { show: true } + } + }, + tooltip: { + enabled: true, + y: { + formatter: function (val: number) { + return formatCurrency(val) + } + } + }, + yaxis: { + min: 0, + max: calculatedMaxValue, + tickAmount: 7, + labels: { + style: { + colors: disabledText, + fontSize: '12px', + fontWeight: '400' + }, + formatter: function (val: number) { + return formatYAxisLabel(val) + } + } + }, + xaxis: { + axisBorder: { show: false }, + axisTicks: { show: false }, + crosshairs: { + stroke: { color: divider } + }, + labels: { + style: { + colors: disabledText, + fontSize: '12px', + fontWeight: '400' + } + }, + categories: categories + }, + legend: { + show: false + } + } + + return ( + + + + + + {title} + + + {accountNumber} + + + {showButton && ( + + )} + + + + {balances.map((balance, index) => ( + + + {formatBalance(balance.amount)} + + + {balance.label} + + + ))} + + + } + sx={{ + pb: 0, + '& .MuiCardHeader-content': { + width: '100%' + } + }} + /> + + + + + ) +} + +export default CashBankCard diff --git a/src/views/apps/cash-bank/CashBankList.tsx b/src/views/apps/cash-bank/CashBankList.tsx new file mode 100644 index 0000000..740fe03 --- /dev/null +++ b/src/views/apps/cash-bank/CashBankList.tsx @@ -0,0 +1,326 @@ +'use client' + +import { useState, useMemo, useEffect } from 'react' +import Grid from '@mui/material/Grid2' +import TextField, { TextFieldProps } from '@mui/material/TextField' +import InputAdornment from '@mui/material/InputAdornment' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import FormControl from '@mui/material/FormControl' +import InputLabel from '@mui/material/InputLabel' +import Select from '@mui/material/Select' +import MenuItem from '@mui/material/MenuItem' +import CashBankCard from './CashBankCard' // Adjust import path as needed +import CustomTextField from '@/@core/components/mui/TextField' + +// Types +interface BankAccount { + id: string + title: string + accountNumber: string + balances: Array<{ + amount: string | number + label: string + }> + chartData: Array<{ + name: string + data: number[] + }> + categories: string[] + chartColor?: string + currency: 'IDR' | 'USD' | 'EUR' + accountType: 'giro' | 'savings' | 'investment' | 'credit' | 'cash' + bank: string + status: 'active' | 'inactive' | 'blocked' +} + +// Dummy Data +const dummyAccounts: BankAccount[] = [ + { + id: '1', + title: 'Giro', + accountNumber: '1-10003', + balances: [ + { amount: '7.313.321', label: 'Saldo di bank' }, + { amount: '30.631.261', label: 'Saldo di kledo' } + ], + chartData: [ + { + name: 'Saldo', + data: [ + 20000000, 21000000, 20500000, 20800000, 21500000, 22000000, 25000000, 26000000, 28000000, 29000000, 30000000, + 31000000 + ] + } + ], + categories: ['Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', 'Jan', 'Feb', 'Mar'], + chartColor: '#ff6b9d', + currency: 'IDR', + accountType: 'giro', + bank: 'Bank Mandiri', + status: 'active' + }, + { + id: '2', + title: 'Tabungan Premium', + accountNumber: 'SAV-001234', + balances: [ + { amount: 15420000, label: 'Saldo Tersedia' }, + { amount: 18750000, label: 'Total Saldo' } + ], + chartData: [ + { + name: 'Balance', + data: [ + 12000000, 13500000, 14200000, 15000000, 15800000, 16200000, 17000000, 17500000, 18000000, 18200000, 18500000, + 18750000 + ] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + chartColor: '#4285f4', + currency: 'IDR', + accountType: 'savings', + bank: 'Bank BCA', + status: 'active' + }, + { + id: '3', + title: 'Investment Portfolio', + accountNumber: 'INV-789012', + balances: [ + { amount: 125000, label: 'Portfolio Value' }, + { amount: 8750, label: 'Total Gains' } + ], + chartData: [ + { + name: 'Portfolio Value', + data: [110000, 115000, 112000, 118000, 122000, 119000, 125000, 128000, 126000, 130000, 127000, 125000] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + currency: 'USD', + accountType: 'investment', + bank: 'Charles Schwab', + status: 'active' + }, + { + id: '4', + title: 'Kartu Kredit Platinum', + accountNumber: 'CC-456789', + balances: [ + { amount: 2500000, label: 'Saldo Saat Ini' }, + { amount: 47500000, label: 'Limit Tersedia' } + ], + chartData: [ + { + name: 'Spending', + data: [ + 1200000, 1800000, 2200000, 1900000, 2100000, 2400000, 2800000, 2600000, 2300000, 2500000, 2700000, 2500000 + ] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + currency: 'IDR', + accountType: 'credit', + bank: 'Bank BNI', + status: 'active' + }, + { + id: '5', + title: 'Deposito Berjangka', + accountNumber: 'DEP-334455', + balances: [ + { amount: 50000000, label: 'Pokok Deposito' }, + { amount: 2500000, label: 'Bunga Terkumpul' } + ], + chartData: [ + { + name: 'Deposito Growth', + data: [ + 50000000, 50200000, 50420000, 50650000, 50880000, 51120000, 51360000, 51610000, 51860000, 52120000, 52380000, + 52500000 + ] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + currency: 'IDR', + accountType: 'savings', + bank: 'Bank BRI', + status: 'active' + }, + { + id: '6', + title: 'Cash Management', + accountNumber: 'CSH-111222', + balances: [{ amount: 5000, label: 'Available Cash' }], + chartData: [ + { + name: 'Cash Flow', + data: [4000, 4500, 4200, 4800, 5200, 4900, 5000, 5300, 5100, 5400, 5200, 5000] + } + ], + categories: ['Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4'], + chartColor: '#00bcd4', + currency: 'USD', + accountType: 'cash', + bank: 'Wells Fargo', + status: 'active' + }, + { + id: '7', + title: 'Rekening Bisnis', + accountNumber: 'BIZ-998877', + balances: [ + { amount: 85000000, label: 'Saldo Operasional' }, + { amount: 15000000, label: 'Dana Cadangan' } + ], + chartData: [ + { + name: 'Business Account', + data: [ + 70000000, 75000000, 80000000, 82000000, 85000000, 88000000, 90000000, 87000000, 85000000, 89000000, 92000000, + 100000000 + ] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + chartColor: '#ff9800', + currency: 'IDR', + accountType: 'giro', + bank: 'Bank Mandiri', + status: 'active' + }, + { + id: '8', + title: 'Tabungan Pendidikan', + accountNumber: 'EDU-567890', + balances: [ + { amount: 25000000, label: 'Dana Pendidikan' }, + { amount: 3500000, label: 'Bunga Terkumpul' } + ], + chartData: [ + { + name: 'Education Savings', + data: [ + 20000000, 21000000, 22000000, 23000000, 24000000, 24500000, 25000000, 25500000, 26000000, 27000000, 28000000, + 28500000 + ] + } + ], + categories: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'], + chartColor: '#3f51b5', + currency: 'IDR', + accountType: 'savings', + bank: 'Bank BCA', + status: 'inactive' + } +] +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} +const CashBankList = () => { + const [searchQuery, setSearchQuery] = useState('') + + // Handle button clicks + const handleAccountAction = (accountId: string, action: string) => { + console.log(`Action: ${action} for account: ${accountId}`) + // Implement your action logic here + } + + // Filter and search logic + const filteredAccounts = useMemo(() => { + return dummyAccounts.filter(account => { + const matchesSearch = + account.title.toLowerCase().includes(searchQuery.toLowerCase()) || + account.accountNumber.toLowerCase().includes(searchQuery.toLowerCase()) || + account.bank.toLowerCase().includes(searchQuery.toLowerCase()) + return matchesSearch + }) + }, [searchQuery]) + + return ( + + {/* Search and Filters */} + + + + setSearchQuery(value as string)} + placeholder='Cari ' + className='max-sm:is-full' + /> + + + + + {/* Account Cards */} + + {filteredAccounts.length > 0 ? ( + filteredAccounts.map(account => ( + + handleAccountAction(account.id, account.accountType || 'default')} + /> + + )) + ) : ( + + + + Tidak ada akun yang ditemukan + + + Coba ubah kata kunci pencarian atau filter yang digunakan + + + + )} + + + ) +} + +export default CashBankList From 2c754d96fdb2a770d85c1ad18f1c2d96eb48a1fc Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 17:51:24 +0700 Subject: [PATCH 25/42] refator menu --- .../layout/vertical/VerticalMenu.tsx | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index e963920..66e8c22 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -113,12 +113,17 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].purchase_quotes} - }> - {dictionary['navigation'].list} - + } + exactMatch={false} + activeUrl='/apps/expense' + > + {dictionary['navigation'].expenses} + } + icon={} exactMatch={false} activeUrl='/apps/cash-bank' > @@ -167,14 +172,22 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].list} - }> - {dictionary['navigation'].list} - {/* {dictionary['navigation'].view} */} - - }> - {dictionary['navigation'].list} - {/* {dictionary['navigation'].view} */} - + } + exactMatch={false} + activeUrl='/apps/user/list' + > + {dictionary['navigation'].user} + + } + exactMatch={false} + activeUrl='/apps/vendor/list' + > + {dictionary['navigation'].vendor} + From e267a50946211d11273e4248d40f8eb8bb94fa2a Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 10 Sep 2025 19:42:26 +0700 Subject: [PATCH 26/42] Cash Bank Detail Table --- .../apps/cash-bank/[id]/detail/page.tsx | 7 + src/types/apps/cashBankTypes.ts | 10 + src/views/apps/cash-bank/CashBankCard.tsx | 8 +- src/views/apps/cash-bank/CashBankList.tsx | 11 +- .../cash-bank/detail/CashBankDetailCard.tsx | 62 ++ .../cash-bank/detail/CashBankDetailHeader.tsx | 19 + .../cash-bank/detail/CashBankDetailTable.tsx | 615 ++++++++++++++++++ src/views/apps/cash-bank/detail/index.tsx | 23 + 8 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx create mode 100644 src/types/apps/cashBankTypes.ts create mode 100644 src/views/apps/cash-bank/detail/CashBankDetailCard.tsx create mode 100644 src/views/apps/cash-bank/detail/CashBankDetailHeader.tsx create mode 100644 src/views/apps/cash-bank/detail/CashBankDetailTable.tsx create mode 100644 src/views/apps/cash-bank/detail/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx new file mode 100644 index 0000000..8f2955f --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/page.tsx @@ -0,0 +1,7 @@ +import CashBankDetail from '@/views/apps/cash-bank/detail' + +const CashBankDetailPage = () => { + return +} + +export default CashBankDetailPage diff --git a/src/types/apps/cashBankTypes.ts b/src/types/apps/cashBankTypes.ts new file mode 100644 index 0000000..c790274 --- /dev/null +++ b/src/types/apps/cashBankTypes.ts @@ -0,0 +1,10 @@ +export type CashBankType = { + id: number + date: string + description: string + reference: string + accept: number + send: number + balance: number + status: string +} diff --git a/src/views/apps/cash-bank/CashBankCard.tsx b/src/views/apps/cash-bank/CashBankCard.tsx index 94f1b10..e2116b9 100644 --- a/src/views/apps/cash-bank/CashBankCard.tsx +++ b/src/views/apps/cash-bank/CashBankCard.tsx @@ -13,6 +13,7 @@ import Button from '@mui/material/Button' // Third-party Imports import type { ApexOptions } from 'apexcharts' +import Link from 'next/link' // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) @@ -36,7 +37,7 @@ interface CashBankCardProps { categories: string[] buttonText?: string buttonIcon?: string - onButtonClick?: () => void + href?: string chartColor?: string height?: number currency?: 'IDR' | 'USD' | 'EUR' @@ -51,7 +52,7 @@ const CashBankCard = ({ balances, chartData, categories, - onButtonClick, + href, chartColor = '#ff6b9d', height = 300, currency = 'IDR', @@ -220,7 +221,8 @@ const CashBankCard = ({ + ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('status', { + header: 'Status', + cell: ({ row }) => ( +
+ +
+ ) + }), + columnHelper.accessor('accept', { + header: 'Penerimaan', + cell: ({ row }) => ( + 0 ? 'text-green-600' : 'text-gray-500'}`}> + {row.original.accept > 0 ? formatCurrency(row.original.accept) : '-'} + + ) + }), + columnHelper.accessor('send', { + header: 'Pengeluaran', + cell: ({ row }) => ( + 0 ? 'text-red-600' : 'text-gray-500'}`}> + {row.original.send > 0 ? formatCurrency(row.original.send) : '-'} + + ) + }), + columnHelper.accessor('balance', { + header: 'Saldo', + cell: ({ row }) => ( + = 0 ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(row.original.balance)} + + ) + }) + ], + [] + ) + + const table = useReactTable({ + data: paginatedData as CashBankType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Filter Status Tabs and Range Date */} +
+
+ + +
+
+ +
+ setSearch(value as string)} + placeholder='Cari Transaksi Cash/Bank' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + {/* Subtotal Row */} + + + + + + + + + + + + {/* Total Row */} + + + + + + + + + + + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + Subtotal + + + + {formatCurrency(subtotalAccept)} + + + + {formatCurrency(subtotalSend)} + + + = 0 ? 'text-green-600' : 'text-red-600'}`} + > + {formatCurrency(netBalance)} + +
+ + Total + + + = 0 ? 'text-green-600' : 'text-red-600'}`} + > + {formatCurrency(netBalance)} + +
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default CashBankDetailTable diff --git a/src/views/apps/cash-bank/detail/index.tsx b/src/views/apps/cash-bank/detail/index.tsx new file mode 100644 index 0000000..c213c42 --- /dev/null +++ b/src/views/apps/cash-bank/detail/index.tsx @@ -0,0 +1,23 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' +import CashBankDetailHeader from './CashBankDetailHeader' +import CashBankDetailTable from './CashBankDetailTable' +import CashBankDetailCard from './CashBankDetailCard' + +const CashBankDetail = () => { + return ( + + + + + + + + + + + + ) +} + +export default CashBankDetail From 630d9c0e65ce6077c49f550cc4b7ec0f65dacf3f Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 00:16:00 +0700 Subject: [PATCH 27/42] Account Page --- .../(private)/apps/account/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 8 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/types/apps/accountTypes.ts | 7 + src/views/apps/account/AccountFormDrawer.tsx | 308 ++++++++++ src/views/apps/account/AccountListTable.tsx | 576 ++++++++++++++++++ src/views/apps/account/index.tsx | 19 + 8 files changed, 929 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/account/page.tsx create mode 100644 src/types/apps/accountTypes.ts create mode 100644 src/views/apps/account/AccountFormDrawer.tsx create mode 100644 src/views/apps/account/AccountListTable.tsx create mode 100644 src/views/apps/account/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/account/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/account/page.tsx new file mode 100644 index 0000000..1d47a3e --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/account/page.tsx @@ -0,0 +1,7 @@ +import AccountList from '@/views/apps/account' + +const AccountPage = () => { + return +} + +export default AccountPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 66e8c22..880cd61 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -129,6 +129,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { > {dictionary['navigation'].cash_and_bank} + } + exactMatch={false} + activeUrl='/apps/account' + > + {dictionary['navigation'].account} + }> {dictionary['navigation'].list} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index df083d2..f5948c4 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -124,6 +124,7 @@ "sales_orders": "Orders", "quotes": "Quotes", "expenses": "Expenses", - "cash_and_bank": "Cash & Bank" + "cash_and_bank": "Cash & Bank", + "account": "Account" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 53ddf6d..d080b8c 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -124,6 +124,7 @@ "sales_orders": "Pemesanan", "quotes": "Penawaran", "expenses": "Biaya", - "cash_and_bank": "Kas & Bank" + "cash_and_bank": "Kas & Bank", + "account": "Akun" } } diff --git a/src/types/apps/accountTypes.ts b/src/types/apps/accountTypes.ts new file mode 100644 index 0000000..b4b151e --- /dev/null +++ b/src/types/apps/accountTypes.ts @@ -0,0 +1,7 @@ +export type AccountType = { + id: number + code: string + name: string + category: string + balance: string +} diff --git a/src/views/apps/account/AccountFormDrawer.tsx b/src/views/apps/account/AccountFormDrawer.tsx new file mode 100644 index 0000000..b6f7512 --- /dev/null +++ b/src/views/apps/account/AccountFormDrawer.tsx @@ -0,0 +1,308 @@ +// React Imports +import { useState, useEffect } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' + +// Third-party Imports +import { useForm, Controller } from 'react-hook-form' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' + +// Account Type +export type AccountType = { + id: number + code: string + name: string + category: string + balance: string +} + +type Props = { + open: boolean + handleClose: () => void + accountData?: AccountType[] + setData: (data: AccountType[]) => void + editingAccount?: AccountType | null +} + +type FormValidateType = { + name: string + code: string + category: string + parentAccount?: string +} + +// Categories available for accounts +const accountCategories = [ + 'Kas & Bank', + 'Piutang', + 'Persediaan', + 'Aset Tetap', + 'Hutang', + 'Ekuitas', + 'Pendapatan', + 'Beban' +] + +// Parent accounts (dummy data for dropdown) +const parentAccounts = [ + { id: 1, code: '1-10001', name: 'Kas' }, + { id: 2, code: '1-10002', name: 'Bank BCA' }, + { id: 3, code: '1-10003', name: 'Bank Mandiri' }, + { id: 4, code: '1-10101', name: 'Piutang Usaha' }, + { id: 5, code: '1-10201', name: 'Persediaan Barang' }, + { id: 6, code: '2-20001', name: 'Hutang Usaha' }, + { id: 7, code: '3-30001', name: 'Modal Pemilik' }, + { id: 8, code: '4-40001', name: 'Penjualan' }, + { id: 9, code: '5-50001', name: 'Beban Gaji' } +] + +// Vars +const initialData = { + name: '', + code: '', + category: '', + parentAccount: '' +} + +const AccountFormDrawer = (props: Props) => { + // Props + const { open, handleClose, accountData, setData, editingAccount } = props + + // Determine if we're editing + const isEdit = !!editingAccount + + // Hooks + const { + control, + reset: resetForm, + handleSubmit, + formState: { errors } + } = useForm({ + defaultValues: initialData + }) + + // Reset form when editingAccount changes or drawer opens + useEffect(() => { + if (open) { + if (editingAccount) { + // Populate form with existing data + resetForm({ + name: editingAccount.name, + code: editingAccount.code, + category: editingAccount.category, + parentAccount: '' + }) + } else { + // Reset to initial data for new account + resetForm(initialData) + } + } + }, [editingAccount, open, resetForm]) + + const onSubmit = (data: FormValidateType) => { + if (isEdit && editingAccount) { + // Update existing account + const updatedAccounts = + accountData?.map(account => + account.id === editingAccount.id + ? { + ...account, + code: data.code, + name: data.name, + category: data.category + } + : account + ) || [] + + setData(updatedAccounts) + } else { + // Create new account + const newAccount: AccountType = { + id: accountData?.length ? Math.max(...accountData.map(a => a.id)) + 1 : 1, + code: data.code, + name: data.name, + category: data.category, + balance: '0' + } + + setData([...(accountData ?? []), newAccount]) + } + + handleClose() + resetForm(initialData) + } + + const handleReset = () => { + handleClose() + resetForm(initialData) + } + + return ( + + {/* Sticky Header */} + +
+ {isEdit ? 'Edit Akun' : 'Tambah Akun Baru'} + + + +
+
+ + {/* Scrollable Content */} + +
onSubmit(data))}> +
+ {/* Nama */} +
+ + Nama * + + ( + + )} + /> +
+ + {/* Kode */} +
+ + Kode * + + ( + + )} + /> +
+ + {/* Kategori */} +
+ + Kategori * + + ( + onChange(newValue || '')} + renderInput={params => ( + + )} + isOptionEqualToValue={(option, value) => option === value} + /> + )} + /> +
+ + {/* Sub Akun dari */} +
+ + Sub Akun dari + + ( + `${account.code} ${account.name}` === value) || null} + onChange={(_, newValue) => onChange(newValue ? `${newValue.code} ${newValue.name}` : '')} + getOptionLabel={option => `${option.code} ${option.name}`} + renderInput={params => } + isOptionEqualToValue={(option, value) => + `${option.code} ${option.name}` === `${value.code} ${value.name}` + } + /> + )} + /> +
+
+
+
+ + {/* Sticky Footer */} + +
+ + +
+
+
+ ) +} + +export default AccountFormDrawer diff --git a/src/views/apps/account/AccountListTable.tsx b/src/views/apps/account/AccountListTable.tsx new file mode 100644 index 0000000..5746c94 --- /dev/null +++ b/src/views/apps/account/AccountListTable.tsx @@ -0,0 +1,576 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import AccountFormDrawer from './AccountFormDrawer' + +// Account Type +export type AccountType = { + id: number + code: string + name: string + category: string + balance: string +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type AccountTypeWithAction = AccountType & { + actions?: string +} + +// Dummy Account Data +const accountsData: AccountType[] = [ + { + id: 1, + code: '1-10001', + name: 'Kas', + category: 'Kas & Bank', + balance: '20000000' + }, + { + id: 2, + code: '1-10002', + name: 'Bank BCA', + category: 'Kas & Bank', + balance: '150000000' + }, + { + id: 3, + code: '1-10003', + name: 'Bank Mandiri', + category: 'Kas & Bank', + balance: '75000000' + }, + { + id: 4, + code: '1-10101', + name: 'Piutang Usaha', + category: 'Piutang', + balance: '50000000' + }, + { + id: 5, + code: '1-10102', + name: 'Piutang Karyawan', + category: 'Piutang', + balance: '5000000' + }, + { + id: 6, + code: '1-10201', + name: 'Persediaan Barang', + category: 'Persediaan', + balance: '100000000' + }, + { + id: 7, + code: '1-10301', + name: 'Peralatan Kantor', + category: 'Aset Tetap', + balance: '25000000' + }, + { + id: 8, + code: '1-10302', + name: 'Kendaraan', + category: 'Aset Tetap', + balance: '200000000' + }, + { + id: 9, + code: '2-20001', + name: 'Hutang Usaha', + category: 'Hutang', + balance: '-30000000' + }, + { + id: 10, + code: '2-20002', + name: 'Hutang Gaji', + category: 'Hutang', + balance: '-15000000' + }, + { + id: 11, + code: '3-30001', + name: 'Modal Pemilik', + category: 'Ekuitas', + balance: '500000000' + }, + { + id: 12, + code: '4-40001', + name: 'Penjualan', + category: 'Pendapatan', + balance: '250000000' + }, + { + id: 13, + code: '5-50001', + name: 'Beban Gaji', + category: 'Beban', + balance: '-80000000' + }, + { + id: 14, + code: '5-50002', + name: 'Beban Listrik', + category: 'Beban', + balance: '-5000000' + }, + { + id: 15, + code: '5-50003', + name: 'Beban Telepon', + category: 'Beban', + balance: '-2000000' + } +] + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Category color mapping for Accounts +const getCategoryColor = (category: string) => { + switch (category) { + case 'Kas & Bank': + return 'success' + case 'Piutang': + return 'info' + case 'Persediaan': + return 'warning' + case 'Aset Tetap': + return 'primary' + case 'Hutang': + return 'error' + case 'Ekuitas': + return 'secondary' + case 'Pendapatan': + return 'success' + case 'Beban': + return 'error' + default: + return 'default' + } +} + +// Format currency +const formatCurrency = (amount: string) => { + const numAmount = parseInt(amount) + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(Math.abs(numAmount)) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const AccountListTable = () => { + const dispatch = useDispatch() + + // States + const [addAccountOpen, setAddAccountOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [accountId, setAccountId] = useState('') + const [search, setSearch] = useState('') + const [categoryFilter, setCategoryFilter] = useState('Semua') + const [filteredData, setFilteredData] = useState(accountsData) + const [data, setData] = useState(accountsData) + const [editingAccount, setEditingAccount] = useState(null) + + // Hooks + const { lang: locale } = useParams() + + // Get unique categories for filter + const categories = useMemo(() => { + const uniqueCategories = [...new Set(data.map(account => account.category))] + return ['Semua', ...uniqueCategories] + }, [data]) + + // Filter data based on search and category + useEffect(() => { + let filtered = data + + // Filter by search + if (search) { + filtered = filtered.filter( + account => + account.code.toLowerCase().includes(search.toLowerCase()) || + account.name.toLowerCase().includes(search.toLowerCase()) || + account.category.toLowerCase().includes(search.toLowerCase()) + ) + } + + // Filter by category + if (categoryFilter !== 'Semua') { + filtered = filtered.filter(account => account.category === categoryFilter) + } + + setFilteredData(filtered) + setCurrentPage(0) + }, [search, categoryFilter, data]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + // Handle row click for edit + const handleRowClick = (account: AccountType, event: React.MouseEvent) => { + // Don't trigger row click if clicking on checkbox or link + const target = event.target as HTMLElement + if (target.closest('input[type="checkbox"]') || target.closest('a') || target.closest('button')) { + return + } + + setEditingAccount(account) + setAddAccountOpen(true) + } + + // Handle closing drawer and reset editing state + const handleCloseDrawer = () => { + setAddAccountOpen(false) + setEditingAccount(null) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('code', { + header: 'Kode Akun', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('name', { + header: 'Nama Akun', + cell: ({ row }) => ( + + {row.original.name} + + ) + }), + columnHelper.accessor('category', { + header: 'Kategori', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('balance', { + header: 'Saldo', + cell: ({ row }) => { + const balance = parseInt(row.original.balance) + return ( + + {balance < 0 ? '-' : ''} + {formatCurrency(row.original.balance)} + + ) + } + }) + ], + [locale] + ) + + const table = useReactTable({ + data: paginatedData as AccountType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + +
+ setSearch(value as string)} + placeholder='Cari Akun' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + handleRowClick(row.original, e)} + > + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + + ) +} + +export default AccountListTable diff --git a/src/views/apps/account/index.tsx b/src/views/apps/account/index.tsx new file mode 100644 index 0000000..783a875 --- /dev/null +++ b/src/views/apps/account/index.tsx @@ -0,0 +1,19 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports + +// Component Imports +import AccountListTable from './AccountListTable' + +const AccountList = () => { + return ( + + + + + + ) +} + +export default AccountList From 63eea38d48fc8fb8651c344cd31e5aac8a34e2c1 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 00:44:07 +0700 Subject: [PATCH 28/42] update --- src/views/apps/account/AccountListTable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/apps/account/AccountListTable.tsx b/src/views/apps/account/AccountListTable.tsx index 5746c94..24b14a6 100644 --- a/src/views/apps/account/AccountListTable.tsx +++ b/src/views/apps/account/AccountListTable.tsx @@ -372,8 +372,6 @@ const AccountListTable = () => { variant='text' color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' - component={Link} - href={getLocalizedUrl(`/apps/accounting/accounts/${row.original.id}/detail`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500, From 42b95bb2129794aa2758879f67ca18ab5091ed75 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 13:37:52 +0700 Subject: [PATCH 29/42] Fixed Asset Table --- .../(private)/apps/fixed-assets/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 8 + src/data/dictionaries/en.json | 3 +- src/data/dictionaries/id.json | 3 +- src/types/apps/fixedAssetTypes.ts | 8 + .../apps/fixed-assets/FixedAssetCard.tsx | 63 +++ .../apps/fixed-assets/FixedAssetTable.tsx | 512 ++++++++++++++++++ src/views/apps/fixed-assets/index.tsx | 18 + 8 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/page.tsx create mode 100644 src/types/apps/fixedAssetTypes.ts create mode 100644 src/views/apps/fixed-assets/FixedAssetCard.tsx create mode 100644 src/views/apps/fixed-assets/FixedAssetTable.tsx create mode 100644 src/views/apps/fixed-assets/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/page.tsx new file mode 100644 index 0000000..fe82eda --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/page.tsx @@ -0,0 +1,7 @@ +import FixedAssetList from '@/views/apps/fixed-assets' + +const FixedAssetPage = () => { + return +} + +export default FixedAssetPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 880cd61..3c71659 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -137,6 +137,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { > {dictionary['navigation'].account} + } + exactMatch={false} + activeUrl='/apps/fixed-assets' + > + {dictionary['navigation'].fixed_assets} + }> {dictionary['navigation'].list} diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index f5948c4..2194f85 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -125,6 +125,7 @@ "quotes": "Quotes", "expenses": "Expenses", "cash_and_bank": "Cash & Bank", - "account": "Account" + "account": "Account", + "fixed_assets": "Fixed Assets" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index d080b8c..054b387 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -125,6 +125,7 @@ "quotes": "Penawaran", "expenses": "Biaya", "cash_and_bank": "Kas & Bank", - "account": "Akun" + "account": "Akun", + "fixed_assets": "Aset Tetap" } } diff --git a/src/types/apps/fixedAssetTypes.ts b/src/types/apps/fixedAssetTypes.ts new file mode 100644 index 0000000..8416aff --- /dev/null +++ b/src/types/apps/fixedAssetTypes.ts @@ -0,0 +1,8 @@ +export type FixedAssetType = { + id: number + assetName: string + puchaseBill: string // Code Purchase + reference: string + date: string + price: number +} diff --git a/src/views/apps/fixed-assets/FixedAssetCard.tsx b/src/views/apps/fixed-assets/FixedAssetCard.tsx new file mode 100644 index 0000000..1a15e48 --- /dev/null +++ b/src/views/apps/fixed-assets/FixedAssetCard.tsx @@ -0,0 +1,63 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + // Fixed Assets Data (from the image) + { + title: 'Nilai Aset', + stats: 'Rp 17.900.000', + avatarIcon: 'tabler-building-store', + avatarColor: 'success', + trend: 'positive', + trendNumber: '100%', + subtitle: 'Hari ini vs 365 hari lalu' + }, + { + title: 'Depresiasi Aset', + stats: 'Rp 0', + avatarIcon: 'tabler-trending-down', + avatarColor: 'secondary', + trend: 'neutral', + trendNumber: '0%', + subtitle: 'Tahun ini vs tanggal sama tahun lalu' + }, + { + title: 'Laba/Rugi Pelepasan Aset', + stats: 'Rp 0', + avatarIcon: 'tabler-exchange', + avatarColor: 'secondary', + trend: 'neutral', + trendNumber: '0%', + subtitle: 'Tahun ini vs tanggal sama tahun lalu' + }, + { + title: 'Aset Baru', + stats: 'Rp 17.900.000', + avatarIcon: 'tabler-plus-circle', + avatarColor: 'success', + trend: 'positive', + trendNumber: '100%', + subtitle: 'Tahun ini vs tanggal sama tahun lalu' + } +] + +const FixedAssetCards = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default FixedAssetCards diff --git a/src/views/apps/fixed-assets/FixedAssetTable.tsx b/src/views/apps/fixed-assets/FixedAssetTable.tsx new file mode 100644 index 0000000..f8226ba --- /dev/null +++ b/src/views/apps/fixed-assets/FixedAssetTable.tsx @@ -0,0 +1,512 @@ +'use client' + +// React Imports +import { useCallback, useEffect, useMemo, useState } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Checkbox from '@mui/material/Checkbox' +import Chip from '@mui/material/Chip' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import { styled } from '@mui/material/styles' +import type { TextFieldProps } from '@mui/material/TextField' +import Typography from '@mui/material/Typography' + +// Third-party Imports +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import { rankItem } from '@tanstack/match-sorter-utils' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import classnames from 'classnames' + +// Type Imports +import type { Locale } from '@configs/i18n' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' +import OptionMenu from '@core/components/option-menu' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import { Box, CircularProgress, TablePagination } from '@mui/material' +import { useDispatch } from 'react-redux' +import TablePaginationComponent from '@/components/TablePaginationComponent' +import Loading from '@/components/layout/shared/Loading' +import { getLocalizedUrl } from '@/utils/i18n' +import StatusFilterTabs from '@/components/StatusFilterTab' + +// Fixed Asset Type +export type FixedAssetType = { + id: number + assetName: string + puchaseBill: string // Code Purchase + reference: string + date: string + price: number +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type FixedAssetTypeWithAction = FixedAssetType & { + actions?: string +} + +// Dummy data for fixed assets +const fixedAssetData: FixedAssetType[] = [ + { + id: 1, + assetName: 'Laptop Dell XPS 13', + puchaseBill: 'PB-2024-001', + reference: 'REF-001', + date: '2024-01-15', + price: 15000000 + }, + { + id: 2, + assetName: 'Office Furniture Set', + puchaseBill: 'PB-2024-002', + reference: 'REF-002', + date: '2024-02-10', + price: 8500000 + }, + { + id: 3, + assetName: 'Printer Canon ImageClass', + puchaseBill: 'PB-2024-003', + reference: 'REF-003', + date: '2024-02-20', + price: 3200000 + }, + { + id: 4, + assetName: 'Air Conditioning Unit', + puchaseBill: 'PB-2024-004', + reference: 'REF-004', + date: '2024-03-05', + price: 12000000 + }, + { + id: 5, + assetName: 'Conference Room TV', + puchaseBill: 'PB-2024-005', + reference: 'REF-005', + date: '2024-03-15', + price: 7500000 + }, + { + id: 6, + assetName: 'MacBook Pro 16"', + puchaseBill: 'PB-2024-006', + reference: 'REF-006', + date: '2024-04-01', + price: 28000000 + }, + { + id: 7, + assetName: 'Standing Desk Electric', + puchaseBill: 'PB-2024-007', + reference: 'REF-007', + date: '2024-04-15', + price: 4500000 + }, + { + id: 8, + assetName: 'Server HP ProLiant', + puchaseBill: 'PB-2024-008', + reference: 'REF-008', + date: '2024-05-01', + price: 45000000 + } +] + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(amount) +} + +// Format date +const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('id-ID', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const FixedAssetTable = () => { + const dispatch = useDispatch() + + // States + const [addAssetOpen, setAddAssetOpen] = useState(false) + const [rowSelection, setRowSelection] = useState({}) + const [currentPage, setCurrentPage] = useState(0) + const [pageSize, setPageSize] = useState(10) + const [openConfirm, setOpenConfirm] = useState(false) + const [assetId, setAssetId] = useState('') + const [search, setSearch] = useState('') + const [filteredData, setFilteredData] = useState([]) + const [statusFilter, setStatusFilter] = useState('Draft') + + // Hooks + const { lang: locale } = useParams() + + // Initialize data on component mount + useEffect(() => { + console.log('Initial fixedAssetData:', fixedAssetData) + setFilteredData(fixedAssetData) + }, []) + + // Filter data based on search + useEffect(() => { + let filtered = [...fixedAssetData] + + // Filter by search + if (search) { + filtered = filtered.filter( + asset => + asset.assetName.toLowerCase().includes(search.toLowerCase()) || + asset.puchaseBill.toLowerCase().includes(search.toLowerCase()) || + asset.reference.toLowerCase().includes(search.toLowerCase()) + ) + } + + console.log('Filtered data:', filtered) // Debug log + setFilteredData(filtered) + setCurrentPage(0) + }, [search]) + + const totalCount = filteredData.length + const paginatedData = useMemo(() => { + const startIndex = currentPage * pageSize + return filteredData.slice(startIndex, startIndex + pageSize) + }, [filteredData, currentPage, pageSize]) + + // Calculate total value from filtered data + const totalValue = useMemo(() => { + return filteredData.reduce((sum, asset) => sum + asset.price, 0) + }, [filteredData]) + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(0) + }, []) + + const handleDelete = () => { + setOpenConfirm(false) + } + + const handleAssetClick = (assetId: string) => { + console.log('Navigasi ke detail Asset:', assetId) + } + + const handleStatusFilter = (status: string) => { + setStatusFilter(status) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('puchaseBill', { + header: 'Kode Pembelian', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('assetName', { + header: 'Nama Aset', + cell: ({ row }) => ( + + {row.original.assetName} + + ) + }), + columnHelper.accessor('reference', { + header: 'Referensi', + cell: ({ row }) => {row.original.reference || '-'} + }), + columnHelper.accessor('date', { + header: 'Tanggal Pembelian', + cell: ({ row }) => {formatDate(row.original.date)} + }), + columnHelper.accessor('price', { + header: 'Harga', + cell: ({ row }) => ( + {formatCurrency(row.original.price)} + ) + }) + ], + [locale] + ) + + const table = useReactTable({ + data: paginatedData as FixedAssetType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + + {/* Header */} +
+ +
+ +
+ setSearch(value as string)} + placeholder='Cari Aset Tetap' + className='max-sm:is-full' + /> +
+ + 10 + 25 + 50 + + + +
+
+ +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {filteredData.length === 0 ? ( + + + + + + ) : ( + + {table.getRowModel().rows.map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + {/* Total Row */} + + + + + + + + + + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + Total Nilai Aset + + + + {formatCurrency(totalValue)} + +
+
+ + ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + /> +
+ + ) +} + +export default FixedAssetTable diff --git a/src/views/apps/fixed-assets/index.tsx b/src/views/apps/fixed-assets/index.tsx new file mode 100644 index 0000000..2de61a9 --- /dev/null +++ b/src/views/apps/fixed-assets/index.tsx @@ -0,0 +1,18 @@ +import Grid from '@mui/material/Grid2' +import FixedAssetCards from './FixedAssetCard' +import FixedAssetTable from './FixedAssetTable' + +const FixedAssetList = () => { + return ( + + + + + + + + + ) +} + +export default FixedAssetList From 9c1b1fc1db296bbe239b939ccda49bf990178199 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 14:47:57 +0700 Subject: [PATCH 30/42] Fixed Asset Add --- .../(private)/apps/fixed-assets/add/page.tsx | 18 + .../apps/fixed-assets/FixedAssetTable.tsx | 2 +- .../fixed-assets/add/FixedAssetAddForm.tsx | 561 ++++++++++++++++++ .../fixed-assets/add/FixedAssetAddHeader.tsx | 19 + 4 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/add/page.tsx create mode 100644 src/views/apps/fixed-assets/add/FixedAssetAddForm.tsx create mode 100644 src/views/apps/fixed-assets/add/FixedAssetAddHeader.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/add/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/add/page.tsx new file mode 100644 index 0000000..adbad6a --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/fixed-assets/add/page.tsx @@ -0,0 +1,18 @@ +import FixedAssetAddForm from '@/views/apps/fixed-assets/add/FixedAssetAddForm' +import FixedAssetAddHeader from '@/views/apps/fixed-assets/add/FixedAssetAddHeader' +import Grid from '@mui/material/Grid2' + +const FixedAssetPage = () => { + return ( + + + + + + + + + ) +} + +export default FixedAssetPage diff --git a/src/views/apps/fixed-assets/FixedAssetTable.tsx b/src/views/apps/fixed-assets/FixedAssetTable.tsx index f8226ba..3f5bb1b 100644 --- a/src/views/apps/fixed-assets/FixedAssetTable.tsx +++ b/src/views/apps/fixed-assets/FixedAssetTable.tsx @@ -409,7 +409,7 @@ const FixedAssetTable = () => { component={Link} className='max-sm:is-full is-auto' startIcon={} - href={getLocalizedUrl('/apps/fixed-asset/add', locale as Locale)} + href={getLocalizedUrl('/apps/fixed-assets/add', locale as Locale)} > Tambah Aset diff --git a/src/views/apps/fixed-assets/add/FixedAssetAddForm.tsx b/src/views/apps/fixed-assets/add/FixedAssetAddForm.tsx new file mode 100644 index 0000000..e83c855 --- /dev/null +++ b/src/views/apps/fixed-assets/add/FixedAssetAddForm.tsx @@ -0,0 +1,561 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent, Typography, Button, Switch, FormControlLabel, Box, Radio } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CustomTextField from '@/@core/components/mui/TextField' +import CustomAutocomplete from '@/@core/components/mui/Autocomplete' + +interface FormData { + namaAset: string + nomor: string + tanggalPembelian: string + hargaBeli: string + akunAsetTetap: any + dikreditkanDariAkun: any + deskripsi: string + referensi: string + tanpaPenyusutan: boolean + akunAkumulasiPenyusutan: any + akunPenyusutan: any + nilaiPenyusuanPerTahun: string + masaManfaatTahun: string + masaManfaatBulan: string + depreciationType: 'percentage' | 'useful-life' + showAdditionalOptions: boolean + metodePenyusutan: any + tanggalMulaiPenyusutan: string + akumulasiPenyusutan: string + batasBiaya: string + nilaiResidu: string + showImageUpload: boolean + uploadedImage: string | null +} + +// Simple ImageUpload component +const ImageUpload = ({ onUpload, maxFileSize, showUrlOption, dragDropText, browseButtonText }: any) => { + const [dragOver, setDragOver] = useState(false) + + const handleFileSelect = (file: File) => { + if (file.size > maxFileSize) { + alert(`File size exceeds ${maxFileSize / (1024 * 1024)}MB limit`) + return + } + + const url = URL.createObjectURL(file) + onUpload(file, url) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragOver(false) + const files = Array.from(e.dataTransfer.files) + if (files[0]) { + handleFileSelect(files[0]) + } + } + + const handleFileInput = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []) + if (files[0]) { + handleFileSelect(files[0]) + } + } + + return ( + { + e.preventDefault() + setDragOver(true) + }} + onDragLeave={() => setDragOver(false)} + > + + {dragDropText} + + + + + ) +} + +const FixedAssetAddForm = () => { + const [formData, setFormData] = useState({ + namaAset: '', + nomor: 'FA/00003', + tanggalPembelian: '11/09/2025', + hargaBeli: '0', + akunAsetTetap: null, + dikreditkanDariAkun: null, + deskripsi: '', + referensi: '', + tanpaPenyusutan: true, + akunAkumulasiPenyusutan: null, + akunPenyusutan: null, + nilaiPenyusuanPerTahun: '', + masaManfaatTahun: '', + masaManfaatBulan: '', + depreciationType: 'percentage', + showAdditionalOptions: false, + metodePenyusutan: null, + tanggalMulaiPenyusutan: '11/09/2025', + akumulasiPenyusutan: '0', + batasBiaya: '0', + nilaiResidu: '0', + showImageUpload: false, + uploadedImage: null + }) + + const handleInputChange = (field: keyof FormData, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value + })) + } + + // Sample options - replace with your actual data + const akunAsetTetapOptions = [{ label: '1-10705 Aset Tetap - Perlengkapan Kantor', value: '1-10705' }] + + const dikreditkanDariAkunOptions = [{ label: 'Silakan pilih dikreditkan dari akun', value: '' }] + + const akunAkumulasiPenyusutanOptions = [{ label: 'Silakan pilih akun akumulasi penyusutan', value: '' }] + + const akunPenyusutanOptions = [{ label: 'Silakan pilih akbun penyusutan', value: '' }] + + const metodePenyusutanOptions = [ + { label: 'Straight Line', value: 'straight-line' }, + { label: 'Declining Balance', value: 'declining-balance' }, + { label: 'Sum of Years Digits', value: 'sum-of-years' } + ] + + const handleUpload = (file: File, url: string) => { + handleInputChange('uploadedImage', url) + console.log('Image uploaded:', { file, url }) + } + + const handleSubmit = () => { + console.log('Form Data:', formData) + // Handle form submission here + } + + return ( + + + + Detil + + + + + + + {/* Image Upload Section */} + {formData.showImageUpload && ( + + + {formData.uploadedImage && ( + + Uploaded asset + + )} + + )} + + + {/* Left Column */} + + ) => handleInputChange('namaAset', e.target.value)} + required + sx={{ mb: 3 }} + /> + + ) => + handleInputChange('tanggalPembelian', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + required + sx={{ mb: 3 }} + /> + + handleInputChange('akunAsetTetap', newValue)} + renderInput={params => ( + + )} + sx={{ mb: 3 }} + /> + + ) => handleInputChange('deskripsi', e.target.value)} + sx={{ mb: 3 }} + /> + + ) => handleInputChange('referensi', e.target.value)} + /> + + + {/* Right Column */} + + ) => handleInputChange('nomor', e.target.value)} + sx={{ mb: 3 }} + /> + + ) => handleInputChange('hargaBeli', e.target.value)} + sx={{ mb: 3 }} + /> + + handleInputChange('dikreditkanDariAkun', newValue)} + renderInput={params => ( + + )} + sx={{ mb: 3 }} + /> + + + + {/* Penyusutan Section */} + + + Penyusutan + + + handleInputChange('tanpaPenyusutan', e.target.checked)} + color='primary' + /> + } + label='Tanpa penyusutan' + /> + + {/* Show depreciation fields when switch is OFF (false) */} + {!formData.tanpaPenyusutan && ( + + + handleInputChange('akunAkumulasiPenyusutan', newValue)} + renderInput={params => ( + + )} + sx={{ mb: 3 }} + /> + + + + handleInputChange('akunPenyusutan', newValue)} + renderInput={params => ( + + )} + sx={{ mb: 3 }} + /> + + + + + + Nilai penyusutan per tahun * + + + handleInputChange('depreciationType', 'percentage')} + value='percentage' + name='depreciation-method' + color='primary' + size='small' + /> + + ) => + handleInputChange('nilaiPenyusuanPerTahun', e.target.value) + } + disabled={formData.depreciationType !== 'percentage'} + sx={{ + flex: 1, + '& .MuiOutlinedInput-root': { + border: 'none', + '& fieldset': { + border: 'none' + } + } + }} + /> + + + + + + + + + Masa Manfaat * + + + handleInputChange('depreciationType', 'useful-life')} + value='useful-life' + name='depreciation-method' + color='primary' + size='small' + /> + + ) => + handleInputChange('masaManfaatTahun', e.target.value) + } + disabled={formData.depreciationType !== 'useful-life'} + sx={{ + flex: 1, + '& .MuiOutlinedInput-root': { + borderRadius: 0, + borderRight: '1px solid #e0e0e0', + '& fieldset': { + border: 'none' + } + } + }} + /> + ) => + handleInputChange('masaManfaatBulan', e.target.value) + } + disabled={formData.depreciationType !== 'useful-life'} + sx={{ + flex: 1, + '& .MuiOutlinedInput-root': { + borderRadius: 0, + '& fieldset': { + border: 'none' + } + } + }} + /> + + + + + + + + + + {/* Additional Options */} + {formData.showAdditionalOptions && ( + <> + + + Metode Penyusutan * + + handleInputChange('metodePenyusutan', newValue)} + renderInput={params => ( + + )} + sx={{ mb: 3 }} + /> + + + + + Tanggal Mulai Penyusutan + + ) => + handleInputChange('tanggalMulaiPenyusutan', e.target.value) + } + InputLabelProps={{ + shrink: true + }} + sx={{ mb: 3 }} + /> + + + + + Akumulasi Penyusutan + + ) => + handleInputChange('akumulasiPenyusutan', e.target.value) + } + sx={{ mb: 3 }} + /> + + + + + Batas Biaya + + ) => + handleInputChange('batasBiaya', e.target.value) + } + sx={{ mb: 3 }} + /> + + + + + Nilai Residu + + ) => + handleInputChange('nilaiResidu', e.target.value) + } + sx={{ mb: 3 }} + /> + + + )} + + )} + + + {/* Submit Button */} + + + + + + ) +} + +export default FixedAssetAddForm diff --git a/src/views/apps/fixed-assets/add/FixedAssetAddHeader.tsx b/src/views/apps/fixed-assets/add/FixedAssetAddHeader.tsx new file mode 100644 index 0000000..c38957c --- /dev/null +++ b/src/views/apps/fixed-assets/add/FixedAssetAddHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const FixedAssetAddHeader = () => { + return ( +
+
+ + Tambah Asset Tetap + +
+
+ ) +} + +export default FixedAssetAddHeader From c9e260860c0742751a799e8344430914a619ceee Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 15:32:26 +0700 Subject: [PATCH 31/42] Report Page --- .../(private)/apps/report/page.tsx | 7 ++ .../layout/vertical/VerticalMenu.tsx | 8 ++ src/views/apps/report/ReportCard.tsx | 87 +++++++++++++++++++ src/views/apps/report/ReportFinancialList.tsx | 38 ++++++++ src/views/apps/report/ReportHeader.tsx | 19 ++++ src/views/apps/report/index.tsx | 18 ++++ 6 files changed, 177 insertions(+) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/page.tsx create mode 100644 src/views/apps/report/ReportCard.tsx create mode 100644 src/views/apps/report/ReportFinancialList.tsx create mode 100644 src/views/apps/report/ReportHeader.tsx create mode 100644 src/views/apps/report/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/page.tsx new file mode 100644 index 0000000..c6c2230 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/page.tsx @@ -0,0 +1,7 @@ +import ReportList from '@/views/apps/report' + +const ReportPage = () => { + return +} + +export default ReportPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 3c71659..00e3a3d 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -145,6 +145,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { > {dictionary['navigation'].fixed_assets} + } + exactMatch={false} + activeUrl='/apps/report' + > + {dictionary['navigation'].reports} + }> {dictionary['navigation'].list} diff --git a/src/views/apps/report/ReportCard.tsx b/src/views/apps/report/ReportCard.tsx new file mode 100644 index 0000000..77e5e68 --- /dev/null +++ b/src/views/apps/report/ReportCard.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { Card, CardContent, Typography } from '@mui/material' +import Link from 'next/link' +import CustomAvatar, { CustomAvatarProps } from '@/@core/components/mui/Avatar' +import { ThemeColor } from '@/@core/types' + +interface ReportCardProps { + title: string + avatarIcon: string + href?: string + target?: '_blank' | '_self' | '_parent' | '_top' + avatarColor?: ThemeColor + avatarVariant?: CustomAvatarProps['variant'] + avatarSkin?: CustomAvatarProps['skin'] + avatarSize?: number +} + +const ReportCard = (props: ReportCardProps) => { + const { title, avatarIcon, href, target = '_self', avatarColor, avatarVariant, avatarSkin, avatarSize } = props + + const CardComponent = ( + + + + + +
+ + {title} + +
+
+
+ ) + + // If href is provided, wrap with Link + if (href) { + // Check if it's an external link + const isExternal = href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:') + + if (isExternal) { + return ( + + {CardComponent} + + ) + } + + // Internal link using Next.js Link + return ( + + {CardComponent} + + ) + } + + // No link, return card as is + return CardComponent +} + +export default ReportCard diff --git a/src/views/apps/report/ReportFinancialList.tsx b/src/views/apps/report/ReportFinancialList.tsx new file mode 100644 index 0000000..1dd7385 --- /dev/null +++ b/src/views/apps/report/ReportFinancialList.tsx @@ -0,0 +1,38 @@ +import { Container, Typography } from '@mui/material' +import Grid from '@mui/material/Grid2' +import ReportCard from './ReportCard' + +const ReportFinancialList: React.FC = () => { + const financialReports = [ + { + title: 'Arus Kas', + iconClass: 'tabler-cash' + }, + { + title: 'Laba Rugi', + iconClass: 'tabler-cash' + }, + { + title: 'Neraca', + iconClass: 'tabler-cash' + } + ] + + return ( +
+ + Finansial + + + + {financialReports.map((report, index) => ( + + + + ))} + +
+ ) +} + +export default ReportFinancialList diff --git a/src/views/apps/report/ReportHeader.tsx b/src/views/apps/report/ReportHeader.tsx new file mode 100644 index 0000000..005428e --- /dev/null +++ b/src/views/apps/report/ReportHeader.tsx @@ -0,0 +1,19 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +const ReportHeader = () => { + return ( +
+
+ + Laporan + +
+
+ ) +} + +export default ReportHeader diff --git a/src/views/apps/report/index.tsx b/src/views/apps/report/index.tsx new file mode 100644 index 0000000..d99eebc --- /dev/null +++ b/src/views/apps/report/index.tsx @@ -0,0 +1,18 @@ +import Grid from '@mui/material/Grid2' +import ReportHeader from './ReportHeader' +import ReportFinancialList from './ReportFinancialList' + +const ReportList = () => { + return ( + + + + + + + + + ) +} + +export default ReportList From 29defb68c83578657fd80a4ed35d18ae92ceb329 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 16:02:31 +0700 Subject: [PATCH 32/42] Ingredient Detail Unit --- .../products/ingredients/[id]/detail/page.tsx | 7 ++++ .../ingredient/ProductIngredientTable.tsx | 36 +++++++++++++----- .../detail/IngredientDetailInfo.tsx | 30 +++++++++++++++ .../detail/IngredientDetailUnit.tsx | 38 +++++++++++++++++++ .../products/ingredient/detail/index.tsx | 18 +++++++++ 5 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/inventory/products/ingredients/[id]/detail/page.tsx create mode 100644 src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailInfo.tsx create mode 100644 src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx create mode 100644 src/views/apps/ecommerce/products/ingredient/detail/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/inventory/products/ingredients/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/inventory/products/ingredients/[id]/detail/page.tsx new file mode 100644 index 0000000..6ede982 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/inventory/products/ingredients/[id]/detail/page.tsx @@ -0,0 +1,7 @@ +import IngredientDetail from '@/views/apps/ecommerce/products/ingredient/detail' + +const IngredientDetailPage = () => { + return +} + +export default IngredientDetailPage diff --git a/src/views/apps/ecommerce/products/ingredient/ProductIngredientTable.tsx b/src/views/apps/ecommerce/products/ingredient/ProductIngredientTable.tsx index 5f9c063..052c9e5 100644 --- a/src/views/apps/ecommerce/products/ingredient/ProductIngredientTable.tsx +++ b/src/views/apps/ecommerce/products/ingredient/ProductIngredientTable.tsx @@ -38,6 +38,10 @@ import AddProductIngredientDrawer from './AddProductIngredientDrawer' import { useIngredientsMutation } from '../../../../../services/mutations/ingredients' import { useDispatch } from 'react-redux' import { setIngredient } from '../../../../../redux-store/slices/ingredient' +import Link from 'next/link' +import { getLocalizedUrl } from '@/utils/i18n' +import { Locale } from '@/configs/i18n' +import { useParams } from 'next/navigation' declare module '@tanstack/table-core' { interface FilterFns { @@ -110,6 +114,8 @@ const ProductIngredientTable = () => { const [openConfirm, setOpenConfirm] = useState(false) const [search, setSearch] = useState('') + const { lang: locale } = useParams() + // Fetch products with pagination and search const { data, isLoading, error, isFetching } = useIngredients({ page: currentPage, @@ -166,14 +172,23 @@ const ProductIngredientTable = () => { columnHelper.accessor('name', { header: 'Name', cell: ({ row }) => ( -
- {/* */} -
- - {row.original.name || '-'} - -
-
+ ) }), columnHelper.accessor('cost', { @@ -396,7 +411,10 @@ const ProductIngredientTable = () => { /> - setAddIngredientOpen(!addIngredientOpen)} /> + setAddIngredientOpen(!addIngredientOpen)} + /> { + return ( + + + + Tepung Terigu + + +
+ } + subheader={ +
+
+ + Cost: {formatCurrency(5000)} + +
+
+ } + /> +
+ ) +} + +export default IngredientDetailInfo diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx new file mode 100644 index 0000000..823b0d2 --- /dev/null +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material' + +const IngredientDetailUnit = () => { + return ( + + + + + + + Satuan Dasar + + + : Pcs + + + + + + + + ) +} + +export default IngredientDetailUnit diff --git a/src/views/apps/ecommerce/products/ingredient/detail/index.tsx b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx new file mode 100644 index 0000000..88c1d81 --- /dev/null +++ b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx @@ -0,0 +1,18 @@ +import Grid from '@mui/material/Grid2' +import IngredientDetailInfo from './IngredientDetailInfo' +import IngredientDetailUnit from './IngredientDetailUnit' + +const IngredientDetail = () => { + return ( + + + + + + + + + ) +} + +export default IngredientDetail From 5a77d3c2ea2c0c4c034b1f64beb14fc5faf10edd Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 16:22:05 +0700 Subject: [PATCH 33/42] Ingredient Detail --- .../detail/IngedientUnitConversionDrawer.tsx | 370 ++++++++++++++++++ .../detail/IngredientDetailUnit.tsx | 91 +++-- 2 files changed, 431 insertions(+), 30 deletions(-) create mode 100644 src/views/apps/ecommerce/products/ingredient/detail/IngedientUnitConversionDrawer.tsx diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngedientUnitConversionDrawer.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngedientUnitConversionDrawer.tsx new file mode 100644 index 0000000..2d23725 --- /dev/null +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngedientUnitConversionDrawer.tsx @@ -0,0 +1,370 @@ +'use client' +// React Imports +import { useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import Typography from '@mui/material/Typography' +import Divider from '@mui/material/Divider' +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' + +// Third-party Imports +import { useForm, Controller } from 'react-hook-form' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' + +type Props = { + open: boolean + handleClose: () => void + setData?: (data: any) => void +} + +type UnitConversionType = { + satuan: string + quantity: number + unit: string + hargaBeli: number + hargaJual: number + isDefault: boolean +} + +type FormValidateType = { + conversions: UnitConversionType[] +} + +// Vars +const initialConversion: UnitConversionType = { + satuan: 'Box', + quantity: 12, + unit: 'Pcs', + hargaBeli: 3588000, + hargaJual: 5988000, + isDefault: false +} + +const IngedientUnitConversionDrawer = (props: Props) => { + // Props + const { open, handleClose, setData } = props + + // States + const [conversions, setConversions] = useState([initialConversion]) + + // Hooks + const { + control, + reset: resetForm, + handleSubmit, + formState: { errors } + } = useForm({ + defaultValues: { + conversions: [initialConversion] + } + }) + + // Functions untuk konversi unit + const handleTambahBaris = () => { + const newConversion: UnitConversionType = { + satuan: '', + quantity: 0, + unit: '', + hargaBeli: 0, + hargaJual: 0, + isDefault: false + } + setConversions([...conversions, newConversion]) + } + + const handleHapusBaris = (index: number) => { + if (conversions.length > 1) { + const newConversions = conversions.filter((_, i) => i !== index) + setConversions(newConversions) + } + } + + const handleChangeConversion = (index: number, field: keyof UnitConversionType, value: any) => { + const newConversions = [...conversions] + newConversions[index] = { ...newConversions[index], [field]: value } + setConversions(newConversions) + } + + const handleToggleDefault = (index: number) => { + const newConversions = conversions.map((conversion, i) => ({ + ...conversion, + isDefault: i === index + })) + setConversions(newConversions) + } + + const onSubmit = (data: FormValidateType) => { + console.log('Unit conversions:', conversions) + if (setData) { + setData(conversions) + } + handleClose() + } + + const handleReset = () => { + handleClose() + setConversions([initialConversion]) + resetForm({ conversions: [initialConversion] }) + } + + const formatNumber = (value: number) => { + return new Intl.NumberFormat('id-ID').format(value) + } + + const parseNumber = (value: string) => { + return parseInt(value.replace(/\./g, '')) || 0 + } + + return ( + + {/* Sticky Header */} + +
+ Konversi Unit Bahan + + + +
+
+ + {/* Scrollable Content */} + +
onSubmit(data))}> +
+ {/* Header Kolom */} + + + + Satuan + + + + + = + + + + + Jumlah + + + + + Unit + + + + + Harga Beli + + + + + Harga Jual + + + + + Default + + + + + Action + + + + + {/* Baris Konversi */} + {conversions.map((conversion, index) => ( + + + + {index + 1} + + + + {/* Satuan */} + + handleChangeConversion(index, 'satuan', e.target.value)} + > + Box + Kg + Liter + Pack + Pcs + + + + {/* Tanda sama dengan */} + + = + + + {/* Quantity */} + + handleChangeConversion(index, 'quantity', parseInt(e.target.value) || 0)} + /> + + + {/* Unit */} + + handleChangeConversion(index, 'unit', e.target.value)} + > + Pcs + Kg + Gram + Liter + ML + + + + {/* Harga Beli */} + + handleChangeConversion(index, 'hargaBeli', parseNumber(e.target.value))} + /> + + + {/* Harga Jual */} + + handleChangeConversion(index, 'hargaJual', parseNumber(e.target.value))} + /> + + + {/* Default Star */} + + handleToggleDefault(index)} + sx={{ + color: conversion.isDefault ? 'warning.main' : 'grey.400' + }} + > + + + + + {/* Delete Button */} + + {conversions.length > 1 && ( + handleHapusBaris(index)} + sx={{ + color: 'error.main', + border: 1, + borderColor: 'error.main', + '&:hover': { + backgroundColor: 'error.light', + borderColor: 'error.main' + } + }} + > + + + )} + + + ))} + + {/* Tambah Baris Button */} +
+ +
+
+ +
+ + {/* Sticky Footer */} + +
+ + +
+
+
+ ) +} + +export default IngedientUnitConversionDrawer diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx index 823b0d2..05f4784 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailUnit.tsx @@ -1,37 +1,68 @@ -import React from 'react' +'use client' +import React, { useState } from 'react' import { Card, CardContent, CardHeader, Typography, Button, Box, Stack } from '@mui/material' +import IngedientUnitConversionDrawer from './IngedientUnitConversionDrawer' // Sesuaikan dengan path file Anda const IngredientDetailUnit = () => { - return ( - - - - - - - Satuan Dasar - - - : Pcs - - - + // State untuk mengontrol drawer + const [openConversionDrawer, setOpenConversionDrawer] = useState(false) - - - + // Function untuk membuka drawer + const handleOpenConversionDrawer = () => { + setOpenConversionDrawer(true) + } + + // Function untuk menutup drawer + const handleCloseConversionDrawer = () => { + setOpenConversionDrawer(false) + } + + // Function untuk handle data dari drawer (opsional) + const handleSetConversionData = (data: any) => { + console.log('Conversion data received:', data) + // Anda bisa menambahkan logic untuk mengupdate state atau API call di sini + } + + return ( + <> + + + + + + + Satuan Dasar + + + : Pcs + + + + + + + + + {/* Ingredient Unit Conversion Drawer */} + + ) } From b3a41fe0e08a69522135ae0763f227e3102694c8 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 16:55:18 +0700 Subject: [PATCH 34/42] Ingredient Detail Stock Adjustment Drawer --- .../IngredientDetailStockAdjustmentDrawer.tsx | 454 ++++++++++++++++++ .../products/ingredient/detail/index.tsx | 64 ++- 2 files changed, 511 insertions(+), 7 deletions(-) create mode 100644 src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailStockAdjustmentDrawer.tsx diff --git a/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailStockAdjustmentDrawer.tsx b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailStockAdjustmentDrawer.tsx new file mode 100644 index 0000000..216e1e9 --- /dev/null +++ b/src/views/apps/ecommerce/products/ingredient/detail/IngredientDetailStockAdjustmentDrawer.tsx @@ -0,0 +1,454 @@ +'use client' +// React Imports +import { useEffect, useState } from 'react' + +// MUI Imports +import Button from '@mui/material/Button' +import Drawer from '@mui/material/Drawer' +import IconButton from '@mui/material/IconButton' +import MenuItem from '@mui/material/MenuItem' +import Typography from '@mui/material/Typography' +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' +import Radio from '@mui/material/Radio' +import RadioGroup from '@mui/material/RadioGroup' +import FormControlLabel from '@mui/material/FormControlLabel' +import FormControl from '@mui/material/FormControl' + +// Third-party Imports +import { useForm, Controller } from 'react-hook-form' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' + +type Props = { + open: boolean + handleClose: () => void + setData?: (data: any) => void +} + +type StockAdjustmentType = { + tipeStok: 'perhitungan' | 'stokMasukKeluar' + gudang: string + tanggal: string + akun: string + nomor: string + referensi: string + tag: string + qtyTercatat: number + satuan: string + qtyAktual: number + selisih: number + hargaRataRata: number +} + +type FormValidateType = StockAdjustmentType + +// Vars +const initialData: StockAdjustmentType = { + tipeStok: 'perhitungan', + gudang: '', + tanggal: '11/09/2025', + akun: '8-80100 Penyesuaian Persediaan', + nomor: 'SA/00007', + referensi: '', + tag: '', + qtyTercatat: 0, + satuan: 'Pcs', + qtyAktual: 0, + selisih: 0, + hargaRataRata: 0 +} + +const IngredientDetailStockAdjustmentDrawer = (props: Props) => { + // Props + const { open, handleClose, setData } = props + + // States + const [formData, setFormData] = useState(initialData) + + // Hooks + const { + control, + reset: resetForm, + handleSubmit, + watch, + setValue, + formState: { errors } + } = useForm({ + defaultValues: initialData + }) + + // Watch values untuk kalkulasi otomatis + const qtyTercatat = watch('qtyTercatat') + const qtyAktual = watch('qtyAktual') + + // Kalkulasi selisih otomatis + useEffect(() => { + const selisih = qtyAktual - qtyTercatat + setValue('selisih', selisih) + }, [qtyTercatat, qtyAktual, setValue]) + + const onSubmit = (data: FormValidateType) => { + console.log('Stock adjustment data:', data) + if (setData) { + setData(data) + } + handleClose() + } + + const handleReset = () => { + handleClose() + resetForm(initialData) + setFormData(initialData) + } + + const formatNumber = (value: number) => { + return new Intl.NumberFormat('id-ID').format(value) + } + + const parseNumber = (value: string) => { + return parseInt(value.replace(/\./g, '')) || 0 + } + + return ( + + {/* Sticky Header */} + +
+ Penyesuaian Stok + + + +
+
+ + {/* Scrollable Content */} + +
onSubmit(data))}> +
+ {/* Tipe Penyesuaian Stok */} +
+ + * Tipe penyesuaian stok + + ( + + + } + label={ + + Perhitungan Stok + + + + + } + /> + } + label={ + + Stok Masuk / Keluar + + + + + } + /> + + + )} + /> +
+ + {/* Gudang dan Tanggal */} + + + + * Gudang + + ( + + Pilih gudang + Gudang Utama + Gudang Cabang + Gudang Produksi + + )} + /> + + + + * Tanggal + + ( + + )} + /> + + + + {/* Akun dan Nomor */} + + + + * Akun + + + + + ( + + 8-80100 Penyesuaian Persediaan + 8-80200 Penyesuaian Stok Rusak + 8-80300 Penyesuaian Stok Hilang + + )} + /> + + + + Nomor + + + + + } + /> + + + + {/* Referensi dan Tag */} + + + + Referensi + + + + + } + /> + + + + Tag + + + + + ( + + Pilih Tag + Urgent + Regular + Maintenance + + )} + /> + + + + {/* Stock Details Section */} + + + {/* Qty Tercatat */} + + + Qty Tercatat + + ( + field.onChange(parseInt(e.target.value) || 0)} + /> + )} + /> + + + {/* Satuan */} + + + Satuan + + ( + + Pcs + Kg + Liter + Box + + )} + /> + + + {/* Qty Aktual */} + + + Qty Aktual + + ( + field.onChange(parseInt(e.target.value) || 0)} + /> + )} + /> + + + {/* Selisih */} + + + Selisih + + ( + + )} + /> + + + {/* Harga Rata-rata */} + + + Harga Rata-rata + + ( + field.onChange(parseNumber(e.target.value))} + /> + )} + /> + + + +
+ +
+ + {/* Sticky Footer */} + +
+ + +
+
+
+ ) +} + +export default IngredientDetailStockAdjustmentDrawer diff --git a/src/views/apps/ecommerce/products/ingredient/detail/index.tsx b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx index 88c1d81..d3b4ccf 100644 --- a/src/views/apps/ecommerce/products/ingredient/detail/index.tsx +++ b/src/views/apps/ecommerce/products/ingredient/detail/index.tsx @@ -1,17 +1,67 @@ +'use client' + +import { useState } from 'react' import Grid from '@mui/material/Grid2' import IngredientDetailInfo from './IngredientDetailInfo' import IngredientDetailUnit from './IngredientDetailUnit' +import IngredientDetailStockAdjustmentDrawer from './IngredientDetailStockAdjustmentDrawer' // Sesuaikan dengan path file Anda +import { Button } from '@mui/material' const IngredientDetail = () => { + // State untuk mengontrol stock adjustment drawer + const [openStockAdjustmentDrawer, setOpenStockAdjustmentDrawer] = useState(false) + + // Function untuk membuka stock adjustment drawer + const handleOpenStockAdjustmentDrawer = () => { + setOpenStockAdjustmentDrawer(true) + } + + // Function untuk menutup stock adjustment drawer + const handleCloseStockAdjustmentDrawer = () => { + setOpenStockAdjustmentDrawer(false) + } + + // Function untuk handle data dari stock adjustment drawer + const handleSetStockAdjustmentData = (data: any) => { + console.log('Stock adjustment data received:', data) + // Anda bisa menambahkan logic untuk mengupdate state atau API call di sini + // Misalnya: update stock data, refresh ingredient data, dll. + } + return ( - - - + <> + + + + + + + + - - - - + + {/* Stock Adjustment Drawer */} + + ) } From a1d23cad9ef4b7b0ba2b5caa389ade6b3f102522 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 17:57:00 +0700 Subject: [PATCH 35/42] Report Cash Flow --- .../(private)/apps/report/cash-flow/page.tsx | 22 ++++ src/components/report/ReportItem.tsx | 101 ++++++++++++++++ src/components/report/ReportTitle.tsx | 27 +++++ src/views/apps/report/ReportFinancialList.tsx | 18 ++- .../apps/report/cash-flow/ReportCashCard.tsx | 62 ++++++++++ .../cash-flow/ReportCashFlowContent.tsx | 113 ++++++++++++++++++ 6 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx create mode 100644 src/components/report/ReportItem.tsx create mode 100644 src/components/report/ReportTitle.tsx create mode 100644 src/views/apps/report/cash-flow/ReportCashCard.tsx create mode 100644 src/views/apps/report/cash-flow/ReportCashFlowContent.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx new file mode 100644 index 0000000..41d04bf --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx @@ -0,0 +1,22 @@ +import ReportTitle from '@/components/report/ReportTitle' +import ReportCashCard from '@/views/apps/report/cash-flow/ReportCashCard' +import ReportCashFlowContent from '@/views/apps/report/cash-flow/ReportCashFlowContent' +import Grid from '@mui/material/Grid2' + +const CashFlowPage = () => { + return ( + + + + + + + + + + + + ) +} + +export default CashFlowPage diff --git a/src/components/report/ReportItem.tsx b/src/components/report/ReportItem.tsx new file mode 100644 index 0000000..bfa61ec --- /dev/null +++ b/src/components/report/ReportItem.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import { Box, Typography, Paper } from '@mui/material' + +// Types +type ReportItemHeaderProps = { + title: string + date: string +} + +type ReportItemSubheaderProps = { + title: string +} + +type ReportItemProps = { + accountCode: string + accountName: string + amount: number + onClick?: () => void + isSubtitle?: boolean +} + +type ReportItemFooterProps = { + title: string + amount: number + children?: React.ReactNode +} + +// Helper function to format currency +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(amount) +} + +// ReportItemHeader Component +export const ReportItemHeader: React.FC = ({ title, date }) => { + return ( + + + {title} + + + {date} + + + ) +} + +// ReportItemSubheader Component +export const ReportItemSubheader: React.FC = ({ title }) => { + return ( + + + {title} + + + ) +} + +// ReportItem Component +export const ReportItem: React.FC = ({ + accountCode, + accountName, + amount, + onClick, + isSubtitle = true +}) => { + return ( + + + {accountCode} {accountName} + + + {formatCurrency(amount)} + + + ) +} + +// ReportItemFooter Component +export const ReportItemFooter: React.FC = ({ title, amount, children }) => { + return ( + + + + {title} + + + {formatCurrency(amount)} + + + {children && {children}} + + ) +} diff --git a/src/components/report/ReportTitle.tsx b/src/components/report/ReportTitle.tsx new file mode 100644 index 0000000..534f107 --- /dev/null +++ b/src/components/report/ReportTitle.tsx @@ -0,0 +1,27 @@ +'use client' + +// MUI Imports +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' + +interface Props { + title: string +} + +const ReportTitle = ({ title }: Props) => { + return ( +
+
+ + {title} + +
+
+ ) +} + +interface Props { + title: string +} + +export default ReportTitle diff --git a/src/views/apps/report/ReportFinancialList.tsx b/src/views/apps/report/ReportFinancialList.tsx index 1dd7385..0851ed9 100644 --- a/src/views/apps/report/ReportFinancialList.tsx +++ b/src/views/apps/report/ReportFinancialList.tsx @@ -1,20 +1,30 @@ +'use client' + import { Container, Typography } from '@mui/material' import Grid from '@mui/material/Grid2' import ReportCard from './ReportCard' +import { getLocalizedUrl } from '@/utils/i18n' +import { Locale } from '@/configs/i18n' +import { useParams } from 'next/navigation' const ReportFinancialList: React.FC = () => { + const { lang: locale } = useParams() + const financialReports = [ { title: 'Arus Kas', - iconClass: 'tabler-cash' + iconClass: 'tabler-cash', + link: getLocalizedUrl(`/apps/report/cash-flow`, locale as Locale) }, { title: 'Laba Rugi', - iconClass: 'tabler-cash' + iconClass: 'tabler-cash', + link: getLocalizedUrl(`/apps/report/profit-loss`, locale as Locale) }, { title: 'Neraca', - iconClass: 'tabler-cash' + iconClass: 'tabler-cash', + link: getLocalizedUrl(`/apps/report/nerace`, locale as Locale) } ] @@ -27,7 +37,7 @@ const ReportFinancialList: React.FC = () => { {financialReports.map((report, index) => ( - + ))} diff --git a/src/views/apps/report/cash-flow/ReportCashCard.tsx b/src/views/apps/report/cash-flow/ReportCashCard.tsx new file mode 100644 index 0000000..b44195f --- /dev/null +++ b/src/views/apps/report/cash-flow/ReportCashCard.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'Quick Ratio', + stats: '2,4', + avatarIcon: 'tabler-gauge', + avatarColor: 'success', + trend: 'positive', + trendNumber: 'Target 0,2', + subtitle: 'Hari Ini' + }, + { + title: 'Current Ratio', + stats: '1,09', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '7,6%', + subtitle: 'vs bulan sebelumnya' + }, + { + title: 'Debt Equity Ratio', + stats: '0', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '0%', + subtitle: 'vs bulan sebelumnya' + }, + { + title: 'Equity Ratio', + stats: '0,65', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '4,4%', + subtitle: 'vs bulan sebelumnya' + } +] + +const ReportCashCard = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default ReportCashCard diff --git a/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx b/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx new file mode 100644 index 0000000..5606849 --- /dev/null +++ b/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx @@ -0,0 +1,113 @@ +'use client' + +import DateRangePicker from '@/components/RangeDatePicker' +import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' +import { Button, Card, CardContent, Paper } from '@mui/material' +import { useState } from 'react' + +const ReportCashFlowContent = () => { + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + return ( + +
+
+ + +
+
+ + + + {}} /> + {}} /> + {}} /> + {}} /> + + + + + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} + /> + + + + + {}} /> + {}} /> + + + + + {}} + /> + {}} /> + + + + + + + + + + + + + {}} /> + {}} /> + {}} /> + + + + + + + + + {}} /> + {}} + /> + {}} /> + {}} /> + + + + + + +
+ ) +} + +export default ReportCashFlowContent From f2eb142dec69d9e65f6d5534f18bb887e2a580e1 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 18:20:40 +0700 Subject: [PATCH 36/42] Report Profit Loss --- .../apps/report/profit-loss/page.tsx | 22 ++++ .../HorizontalWithSubtitle.tsx | 2 +- src/components/report/ReportItem.tsx | 7 +- .../profit-loss/ReportProfitLossCard.tsx | 98 +++++++++++++++ .../profit-loss/ReportProfitLossContent.tsx | 117 ++++++++++++++++++ 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/profit-loss/page.tsx create mode 100644 src/views/apps/report/profit-loss/ReportProfitLossCard.tsx create mode 100644 src/views/apps/report/profit-loss/ReportProfitLossContent.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/profit-loss/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/profit-loss/page.tsx new file mode 100644 index 0000000..93c519a --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/profit-loss/page.tsx @@ -0,0 +1,22 @@ +import ReportTitle from '@/components/report/ReportTitle' +import ReportProfitLossCard from '@/views/apps/report/profit-loss/ReportProfitLossCard' +import ReportProfitLossContent from '@/views/apps/report/profit-loss/ReportProfitLossContent' +import Grid from '@mui/material/Grid2' + +const ProfiltLossPage = () => { + return ( + + + + + + + + + + + + ) +} + +export default ProfiltLossPage diff --git a/src/components/card-statistics/HorizontalWithSubtitle.tsx b/src/components/card-statistics/HorizontalWithSubtitle.tsx index af663b5..2797957 100644 --- a/src/components/card-statistics/HorizontalWithSubtitle.tsx +++ b/src/components/card-statistics/HorizontalWithSubtitle.tsx @@ -32,7 +32,7 @@ const HorizontalWithSubtitle = (props: UserDataType) => {
{title}
- {stats} + {stats} {`(${trend === 'negative' ? '-' : '+'}${trendNumber})`} diff --git a/src/components/report/ReportItem.tsx b/src/components/report/ReportItem.tsx index bfa61ec..42c570f 100644 --- a/src/components/report/ReportItem.tsx +++ b/src/components/report/ReportItem.tsx @@ -4,7 +4,8 @@ import { Box, Typography, Paper } from '@mui/material' // Types type ReportItemHeaderProps = { title: string - date: string + date?: string + amount?: number } type ReportItemSubheaderProps = { @@ -34,14 +35,14 @@ const formatCurrency = (amount: number) => { } // ReportItemHeader Component -export const ReportItemHeader: React.FC = ({ title, date }) => { +export const ReportItemHeader: React.FC = ({ title, date, amount }) => { return ( {title} - {date} + {amount ? formatCurrency(amount) : date} ) diff --git a/src/views/apps/report/profit-loss/ReportProfitLossCard.tsx b/src/views/apps/report/profit-loss/ReportProfitLossCard.tsx new file mode 100644 index 0000000..a114637 --- /dev/null +++ b/src/views/apps/report/profit-loss/ReportProfitLossCard.tsx @@ -0,0 +1,98 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'Pendapatan', + stats: '29.004.775', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '48,8%', + subtitle: 'vs Bulan Lalu' + }, + { + title: 'Margin Laba Bersih', + stats: '38%', + avatarIcon: 'tabler-gauge', + avatarColor: 'success', + trend: 'positive', + trendNumber: 'Bulan Ini', + subtitle: 'Bulan Ini' + }, + { + title: 'Laba Kotor', + stats: '21.076.389', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '43,5%', + subtitle: 'vs bulan lalu' + }, + { + title: 'Laba Bersih', + stats: '11.111.074', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '36,8%', + subtitle: 'vs bulan lalu' + }, + { + title: 'Margin Laba Kotor', + stats: '73%', + avatarIcon: 'tabler-gauge', + avatarColor: 'success', + trend: 'positive', + trendNumber: 'Bulan Ini', + subtitle: 'Bulan Ini' + }, + { + title: 'Biaya Operasional', + stats: '9.965.315', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '49,4%', + subtitle: 'vs Bulan Lalu' + }, + { + title: 'Rasio Biaya Operasional', + stats: '61,7%', + avatarIcon: 'tabler-gauge', + avatarColor: 'success', + trend: 'positive', + trendNumber: 'Bulan Ini', + subtitle: 'Bulan Ini' + }, + { + title: 'EBITDA', + stats: '11.032.696', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '37,3%', + subtitle: 'vs bulan lalu' + } +] + +const ReportProfitLossCard = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default ReportProfitLossCard diff --git a/src/views/apps/report/profit-loss/ReportProfitLossContent.tsx b/src/views/apps/report/profit-loss/ReportProfitLossContent.tsx new file mode 100644 index 0000000..8bb09ee --- /dev/null +++ b/src/views/apps/report/profit-loss/ReportProfitLossContent.tsx @@ -0,0 +1,117 @@ +'use client' + +import DateRangePicker from '@/components/RangeDatePicker' +import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' +import { Button, Card, CardContent, Paper } from '@mui/material' +import { useState } from 'react' + +const ReportProfitLossContent = () => { + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + return ( + +
+
+ + +
+
+ + + + {}} /> + + {}} + /> + {}} /> + {}} + /> + + + + + {}} /> + {}} /> + + + + + + + + + {}} /> + {}} + /> + {}} /> + {}} + /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} + /> + {}} /> + {}} /> + {}} /> + + + {}} + /> + {}} /> + {}} /> + {}} /> + + + + + +
+ ) +} + +export default ReportProfitLossContent From e13fbab56416f1b7aa91602e40710a0ab64a4bea Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 18:29:17 +0700 Subject: [PATCH 37/42] Report Neraca --- .../(private)/apps/report/neraca/page.tsx | 22 ++++ src/views/apps/report/ReportFinancialList.tsx | 2 +- .../apps/report/neraca/ReportNeracaCard.tsx | 62 ++++++++++ .../report/neraca/ReportNeracaContent.tsx | 113 ++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/neraca/page.tsx create mode 100644 src/views/apps/report/neraca/ReportNeracaCard.tsx create mode 100644 src/views/apps/report/neraca/ReportNeracaContent.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/neraca/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/neraca/page.tsx new file mode 100644 index 0000000..fa21421 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/neraca/page.tsx @@ -0,0 +1,22 @@ +import ReportTitle from '@/components/report/ReportTitle' +import ReportNeracaCard from '@/views/apps/report/neraca/ReportNeracaCard' +import ReportNeracaContent from '@/views/apps/report/neraca/ReportNeracaContent' +import Grid from '@mui/material/Grid2' + +const NeracaPage = () => { + return ( + + + + + + + + + + + + ) +} + +export default NeracaPage diff --git a/src/views/apps/report/ReportFinancialList.tsx b/src/views/apps/report/ReportFinancialList.tsx index 0851ed9..74db207 100644 --- a/src/views/apps/report/ReportFinancialList.tsx +++ b/src/views/apps/report/ReportFinancialList.tsx @@ -24,7 +24,7 @@ const ReportFinancialList: React.FC = () => { { title: 'Neraca', iconClass: 'tabler-cash', - link: getLocalizedUrl(`/apps/report/nerace`, locale as Locale) + link: getLocalizedUrl(`/apps/report/neraca`, locale as Locale) } ] diff --git a/src/views/apps/report/neraca/ReportNeracaCard.tsx b/src/views/apps/report/neraca/ReportNeracaCard.tsx new file mode 100644 index 0000000..3a343ab --- /dev/null +++ b/src/views/apps/report/neraca/ReportNeracaCard.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'Quick Ratio', + stats: '2,4', + avatarIcon: 'tabler-gauge', + avatarColor: 'success', + trend: 'positive', + trendNumber: 'Target 0,2', + subtitle: 'Hari Ini' + }, + { + title: 'Current Ratio', + stats: '1,09', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '7,6%', + subtitle: 'vs bulan sebelumnya' + }, + { + title: 'Debt Equity Ratio', + stats: '0', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '0%', + subtitle: 'vs bulan sebelumnya' + }, + { + title: 'Equity Ratio', + stats: '0,65', + avatarIcon: 'tabler-trending-down', + avatarColor: 'error', + trend: 'negative', + trendNumber: '4,4%', + subtitle: 'vs bulan sebelumnya' + } +] + +const ReportNeracaCard = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default ReportNeracaCard diff --git a/src/views/apps/report/neraca/ReportNeracaContent.tsx b/src/views/apps/report/neraca/ReportNeracaContent.tsx new file mode 100644 index 0000000..63b02a9 --- /dev/null +++ b/src/views/apps/report/neraca/ReportNeracaContent.tsx @@ -0,0 +1,113 @@ +'use client' + +import DateRangePicker from '@/components/RangeDatePicker' +import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' +import { Button, Card, CardContent, Paper } from '@mui/material' +import { useState } from 'react' + +const ReportNeracaContent = () => { + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + return ( + +
+
+ + +
+
+ + + + {}} /> + {}} /> + {}} /> + {}} /> + + + + + {}} /> + {}} /> + {}} /> + {}} /> + {}} /> + {}} + /> + + + + + {}} /> + {}} /> + + + + + {}} + /> + {}} /> + + + + + + + + + + + + + {}} /> + {}} /> + {}} /> + + + + + + + + + {}} /> + {}} + /> + {}} /> + {}} /> + + + + + + +
+ ) +} + +export default ReportNeracaContent From 34442740e22afa875e9df0e2cc5ce21b02221313 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 18:38:27 +0700 Subject: [PATCH 38/42] Report Cash FLow --- .../(private)/apps/report/cash-flow/page.tsx | 4 +- .../apps/report/cash-flow/ReportCashCard.tsx | 62 -------- .../report/cash-flow/ReportCashFlowCard.tsx | 62 ++++++++ .../cash-flow/ReportCashFlowContent.tsx | 147 ++++++++++-------- 4 files changed, 148 insertions(+), 127 deletions(-) delete mode 100644 src/views/apps/report/cash-flow/ReportCashCard.tsx create mode 100644 src/views/apps/report/cash-flow/ReportCashFlowCard.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx index 41d04bf..a544810 100644 --- a/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/cash-flow/page.tsx @@ -1,5 +1,5 @@ import ReportTitle from '@/components/report/ReportTitle' -import ReportCashCard from '@/views/apps/report/cash-flow/ReportCashCard' +import ReportCashFlowCard from '@/views/apps/report/cash-flow/ReportCashFlowCard' import ReportCashFlowContent from '@/views/apps/report/cash-flow/ReportCashFlowContent' import Grid from '@mui/material/Grid2' @@ -10,7 +10,7 @@ const CashFlowPage = () => { - + diff --git a/src/views/apps/report/cash-flow/ReportCashCard.tsx b/src/views/apps/report/cash-flow/ReportCashCard.tsx deleted file mode 100644 index b44195f..0000000 --- a/src/views/apps/report/cash-flow/ReportCashCard.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// MUI Imports -import Grid from '@mui/material/Grid2' - -// Type Imports -import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' - -// Component Imports -import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' - -// Vars -const data: UserDataType[] = [ - { - title: 'Quick Ratio', - stats: '2,4', - avatarIcon: 'tabler-gauge', - avatarColor: 'success', - trend: 'positive', - trendNumber: 'Target 0,2', - subtitle: 'Hari Ini' - }, - { - title: 'Current Ratio', - stats: '1,09', - avatarIcon: 'tabler-trending-down', - avatarColor: 'error', - trend: 'negative', - trendNumber: '7,6%', - subtitle: 'vs bulan sebelumnya' - }, - { - title: 'Debt Equity Ratio', - stats: '0', - avatarIcon: 'tabler-trending-up', - avatarColor: 'success', - trend: 'positive', - trendNumber: '0%', - subtitle: 'vs bulan sebelumnya' - }, - { - title: 'Equity Ratio', - stats: '0,65', - avatarIcon: 'tabler-trending-down', - avatarColor: 'error', - trend: 'negative', - trendNumber: '4,4%', - subtitle: 'vs bulan sebelumnya' - } -] - -const ReportCashCard = () => { - return ( - - {data.map((item, i) => ( - - - - ))} - - ) -} - -export default ReportCashCard diff --git a/src/views/apps/report/cash-flow/ReportCashFlowCard.tsx b/src/views/apps/report/cash-flow/ReportCashFlowCard.tsx new file mode 100644 index 0000000..8c5e707 --- /dev/null +++ b/src/views/apps/report/cash-flow/ReportCashFlowCard.tsx @@ -0,0 +1,62 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' + +// Type Imports +import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' + +// Component Imports +import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' + +// Vars +const data: UserDataType[] = [ + { + title: 'PERUBAHAN KAS', + stats: '14.784.651', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '50,2%', + subtitle: 'Bulan Ini vs bulan sebelumnya' + }, + { + title: 'SALDO PENUTUPAN', + stats: '73.041.637', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '100%', + subtitle: 'Bulan Ini vs 1 bulan lalu' + }, + { + title: 'KAS KELUAR', + stats: '11.484.350', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '93,3%', + subtitle: 'Bulan Ini vs bulan sebelumnya' + }, + { + title: 'KAS MASUK', + stats: '26.269.001', + avatarIcon: 'tabler-trending-up', + avatarColor: 'success', + trend: 'positive', + trendNumber: '92,5%', + subtitle: 'Bulan Ini vs bulan sebelumnya' + } +] + +const ReportCashFlowCard = () => { + return ( + + {data.map((item, i) => ( + + + + ))} + + ) +} + +export default ReportCashFlowCard diff --git a/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx b/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx index 5606849..4d96599 100644 --- a/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx +++ b/src/views/apps/report/cash-flow/ReportCashFlowContent.tsx @@ -30,80 +30,101 @@ const ReportCashFlowContent = () => {
- - - {}} /> - {}} /> - {}} /> - {}} /> - - - - - {}} /> - {}} /> - {}} /> - {}} /> - {}} /> + {}} /> - - - - - {}} /> - {}} /> - - - - {}} /> - {}} /> - - - - - - - - - - - - - {}} /> - {}} /> - {}} /> - - - - - - - - - {}} /> {}} /> - {}} /> - {}} /> - + {}} + /> + {}} + /> + {}} + /> + - + + {}} + /> + {}} + /> + + + + + {}} + /> + {}} /> + + + + + + + + {}} + /> + {}} + /> + From 1a9015563fcb03582aef0ee0dda8675cfab7a228 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 20:43:15 +0700 Subject: [PATCH 39/42] Report --- src/views/apps/report/ReportCard.tsx | 2 +- src/views/apps/report/ReportFinancialList.tsx | 7 ++- src/views/apps/report/ReportPurchaseList.tsx | 58 +++++++++++++++++ src/views/apps/report/ReportSalesList.tsx | 63 +++++++++++++++++++ src/views/apps/report/index.tsx | 8 +++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/views/apps/report/ReportPurchaseList.tsx create mode 100644 src/views/apps/report/ReportSalesList.tsx diff --git a/src/views/apps/report/ReportCard.tsx b/src/views/apps/report/ReportCard.tsx index 77e5e68..093ebb6 100644 --- a/src/views/apps/report/ReportCard.tsx +++ b/src/views/apps/report/ReportCard.tsx @@ -46,7 +46,7 @@ const ReportCard = (props: ReportCardProps) => {
- + {title}
diff --git a/src/views/apps/report/ReportFinancialList.tsx b/src/views/apps/report/ReportFinancialList.tsx index 74db207..9c78c5b 100644 --- a/src/views/apps/report/ReportFinancialList.tsx +++ b/src/views/apps/report/ReportFinancialList.tsx @@ -25,12 +25,17 @@ const ReportFinancialList: React.FC = () => { title: 'Neraca', iconClass: 'tabler-cash', link: getLocalizedUrl(`/apps/report/neraca`, locale as Locale) + }, + { + title: 'Ringkasan Eksekutif', + iconClass: 'tabler-cash', + link: getLocalizedUrl(`/apps/report/neraca`, locale as Locale) } ] return (
- + Finansial diff --git a/src/views/apps/report/ReportPurchaseList.tsx b/src/views/apps/report/ReportPurchaseList.tsx new file mode 100644 index 0000000..a28d799 --- /dev/null +++ b/src/views/apps/report/ReportPurchaseList.tsx @@ -0,0 +1,58 @@ +'use client' + +import { Container, Typography } from '@mui/material' +import Grid from '@mui/material/Grid2' +import ReportCard from './ReportCard' +import { getLocalizedUrl } from '@/utils/i18n' +import { Locale } from '@/configs/i18n' +import { useParams } from 'next/navigation' + +const ReportPurchaseList: React.FC = () => { + const { lang: locale } = useParams() + + const purchaseReports = [ + { + title: 'Detail Pembelian', + iconClass: 'tabler-shopping-cart', + link: '' + }, + { + title: 'Tagihan Vendor', + iconClass: 'tabler-shopping-cart', + link: '' + }, + { + title: 'Pembelian per Produk', + iconClass: 'tabler-shopping-cart', + link: '' + }, + { + title: 'Pembelian per Vendor', + iconClass: 'tabler-shopping-cart', + link: '' + }, + { + title: 'Pembelian Produk per Vendor', + iconClass: 'tabler-shopping-cart', + link: '' + } + ] + + return ( +
+ + Pembelian + + + + {purchaseReports.map((report, index) => ( + + + + ))} + +
+ ) +} + +export default ReportPurchaseList diff --git a/src/views/apps/report/ReportSalesList.tsx b/src/views/apps/report/ReportSalesList.tsx new file mode 100644 index 0000000..cdd78cb --- /dev/null +++ b/src/views/apps/report/ReportSalesList.tsx @@ -0,0 +1,63 @@ +'use client' + +import { Container, Typography } from '@mui/material' +import Grid from '@mui/material/Grid2' +import ReportCard from './ReportCard' +import { getLocalizedUrl } from '@/utils/i18n' +import { Locale } from '@/configs/i18n' +import { useParams } from 'next/navigation' + +const ReportSalesList: React.FC = () => { + const { lang: locale } = useParams() + + const salesReports = [ + { + title: 'Detail Penjualan', + iconClass: 'tabler-receipt-2', + link: '' + }, + { + title: 'Tagihan Pelanggan', + iconClass: 'tabler-receipt-2', + link: '' + }, + { + title: 'Penjualan per Produk', + iconClass: 'tabler-receipt-2', + link: '' + }, + { + title: 'Penjualan per Kategori Produk', + iconClass: 'tabler-receipt-2', + link: '' + }, + { + title: 'Penjualan Produk per Pelanggan', + iconClass: 'tabler-receipt-2', + link: '' + }, + { + title: 'Pemesanan per Produk', + iconClass: 'tabler-receipt-2', + link: '' + } + ] + + return ( +
+ + Penjualan + + + + {salesReports.map((report, index) => ( + + + + ))} + +
+ ) +} + +export default ReportSalesList diff --git a/src/views/apps/report/index.tsx b/src/views/apps/report/index.tsx index d99eebc..87c14ae 100644 --- a/src/views/apps/report/index.tsx +++ b/src/views/apps/report/index.tsx @@ -1,6 +1,8 @@ import Grid from '@mui/material/Grid2' import ReportHeader from './ReportHeader' import ReportFinancialList from './ReportFinancialList' +import ReportSalesList from './ReportSalesList' +import ReportPurchaseList from './ReportPurchaseList' const ReportList = () => { return ( @@ -11,6 +13,12 @@ const ReportList = () => { + + + + + + ) } From 5723d490902ed19d873f1cd67ba911e81c03f9f4 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 21:47:52 +0700 Subject: [PATCH 40/42] Reconciliation and Cash Bank Detail Transaction --- .../transaction/[transaction_id]/page.tsx | 18 + .../cash-bank/detail/CashBankDetailTable.tsx | 2 +- .../CashBankDetailTransactionContent.tsx | 27 ++ .../CashBankDetailTransactionHeader.tsx | 23 ++ .../CashBankDetailTransactionInformation.tsx | 177 +++++++++ .../CashBankDetailTransactionLog.tsx | 59 +++ .../CashBankDetailTransactionReconsiled.tsx | 342 ++++++++++++++++++ 7 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/transaction/[transaction_id]/page.tsx create mode 100644 src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionContent.tsx create mode 100644 src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionHeader.tsx create mode 100644 src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionInformation.tsx create mode 100644 src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionLog.tsx create mode 100644 src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionReconsiled.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/transaction/[transaction_id]/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/transaction/[transaction_id]/page.tsx new file mode 100644 index 0000000..b5b7a0d --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/cash-bank/[id]/detail/transaction/[transaction_id]/page.tsx @@ -0,0 +1,18 @@ +import CashBankDetailTransactionContent from '@/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionContent' +import CashBankDetailTransactionHeader from '@/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionHeader' +import Grid from '@mui/material/Grid2' + +const TransactionPage = () => { + return ( + + + + + + + + + ) +} + +export default TransactionPage diff --git a/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx b/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx index e2bb454..d8b114a 100644 --- a/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx +++ b/src/views/apps/cash-bank/detail/CashBankDetailTable.tsx @@ -338,7 +338,7 @@ const CashBankDetailTable = () => { color='primary' className='p-0 min-w-0 font-medium normal-case justify-start' component={Link} - href={getLocalizedUrl(`/apps/cashbank/${row.original.id}/detail`, locale as Locale)} + href={getLocalizedUrl(`/apps/cash-bank/1-10003/detail/transaction/${row.original.id}`, locale as Locale)} sx={{ textTransform: 'none', fontWeight: 500, diff --git a/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionContent.tsx b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionContent.tsx new file mode 100644 index 0000000..c96cf25 --- /dev/null +++ b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionContent.tsx @@ -0,0 +1,27 @@ +'use client' + +import Grid from '@mui/material/Grid2' +import CashBankDetailTransactionInformation from './CashBankDetailTransactionInformation' +import CashBankDetailTransactionLog from './CashBankDetailTransactionLog' +import CashBankDetailTransactionReconciliationDrawer from './CashBankDetailTransactionReconsiled' +import { useState } from 'react' + +const CashBankDetailTransactionContent = () => { + return ( + <> + + + + + {/* + + */} + + + + + + ) +} + +export default CashBankDetailTransactionContent diff --git a/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionHeader.tsx b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionHeader.tsx new file mode 100644 index 0000000..217d716 --- /dev/null +++ b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionHeader.tsx @@ -0,0 +1,23 @@ +import { Typography } from '@mui/material' + +interface Props { + title: string + transaction: string +} + +const CashBankDetailTransactionHeader = ({ title, transaction }: Props) => { + return ( +
+
+ + {title} + + + Transaksi: {transaction} + +
+
+ ) +} + +export default CashBankDetailTransactionHeader diff --git a/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionInformation.tsx b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionInformation.tsx new file mode 100644 index 0000000..3d8e1a2 --- /dev/null +++ b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionInformation.tsx @@ -0,0 +1,177 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardHeader, CardContent, Typography, Box, Button, IconButton } from '@mui/material' +import Grid from '@mui/material/Grid2' +import CashBankDetailTransactionReconciliationDrawer from './CashBankDetailTransactionReconsiled' + +interface PaymentData { + dari: string + nomor: string + tglTransaksi: string + referensi: string + status: string +} + +interface TransactionItem { + deskripsi: string + total: number +} + +const CashBankDetailTransactionInformation: React.FC = () => { + const [open, setOpen] = useState(false) + + const paymentData: PaymentData = { + dari: 'POS Customer', + nomor: 'IP/00030', + tglTransaksi: '10/09/2025', + referensi: 'Pembayaran INV/01/59A/4CY/00003', + status: 'Unreconciled' + } + + const transactionItems: TransactionItem[] = [ + { + deskripsi: 'Terima pembayaran tagihan INV/01/59A/4CY/00003', + total: 220890 + } + ] + + const totalAmount: number = transactionItems.reduce((sum, item) => sum + item.total, 0) + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('id-ID').format(amount) + } + + return ( + <> + + + + Unreconciled + + + + + + + + } + /> + + + {/* Payment Information */} + + + + + Dari + + + {paymentData.dari} + + + + + + Tanggal Transaksi + + {paymentData.tglTransaksi} + + + + + + + Nomor + + {paymentData.nomor} + + + + + Referensi + + {paymentData.referensi} + + + + + {/* Transaction Items Header */} + + + Deskripsi + + + Total + + + + {/* Transaction Items */} + + {transactionItems.map((item, index) => ( + + + + {item.deskripsi} + + + + + {formatCurrency(item.total)} + + + + ))} + + + {/* Total Section */} + + + + Total + + + {formatCurrency(totalAmount)} + + + + + + setOpen(!open)} /> + + ) +} + +export default CashBankDetailTransactionInformation diff --git a/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionLog.tsx b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionLog.tsx new file mode 100644 index 0000000..cd19d0d --- /dev/null +++ b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionLog.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' +import { Card, CardContent, CardHeader, Typography, Box, Link } from '@mui/material' + +interface LogEntry { + id: string + action: string + timestamp: string + user: string +} + +const CashBankDetailTransactionLog: React.FC = () => { + const logEntries: LogEntry[] = [ + { + id: '1', + action: 'Terakhir diubah oleh', + timestamp: '08 Sep 2025 18:26', + user: 'pada' + } + ] + + return ( + + + Pantau log perubahan data +
+ } + sx={{ pb: 1 }} + /> + + {logEntries.map(entry => ( + + + + + {entry.action} {entry.user} {entry.timestamp} + + + + ))} + + + ) +} + +export default CashBankDetailTransactionLog diff --git a/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionReconsiled.tsx b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionReconsiled.tsx new file mode 100644 index 0000000..681069b --- /dev/null +++ b/src/views/apps/cash-bank/detail/transaction/CashBankDetailTransactionReconsiled.tsx @@ -0,0 +1,342 @@ +import React, { useState } from 'react' +import { + Drawer, + Typography, + Box, + IconButton, + Button, + TextField, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Checkbox, + Paper, + MenuItem, + Select, + FormControl +} from '@mui/material' +import Grid from '@mui/material/Grid2' + +interface Props { + open: boolean + handleClose: () => void +} + +const CashBankDetailTransactionReconciliationDrawer: React.FC = ({ open, handleClose }) => { + const [tanggalDari, setTanggalDari] = useState('10/09/2025') + const [tanggalSampai, setTanggalSampai] = useState('10/09/2025') + const [totalDari, setTotalDari] = useState('220.890') + const [totalSampai, setTotalSampai] = useState('220.890') + const [cari, setCari] = useState('') + + const transactionData = { + detil: '10/09/2025', + kirim: 'Terima pembayaran tagihan: POS Customer INV/01/59A/4CY/00003', + terima: '220.890' + } + + return ( + + {/* Header */} + +
+ + Rekonsiliasi + + + + +
+
+ + {/* Content */} + + + + {/* Mutasi Bank Section */} + + + Mutasi Bank + + + Cari mutasi bank yang sesuai dengan transaksi di Kledo + + + {/* Filter Section */} + + {/* Tanggal */} + + + Tanggal + +
+ setTanggalDari(e.target.value)} + sx={{ width: '150px' }} + /> + s/d + setTanggalSampai(e.target.value)} + sx={{ width: '150px' }} + /> +
+
+ + {/* Total */} + + + Total + +
+ setTotalDari(e.target.value)} + sx={{ width: '150px' }} + /> + s/d + setTotalSampai(e.target.value)} + sx={{ width: '150px' }} + /> +
+
+ + {/* Cari */} + + + Cari + +
+ setCari(e.target.value)} + sx={{ width: '300px' }} + /> + +
+
+
+ + {/* Table */} + + + + + + + + Detil + Kirim + Terima + + + + + +
+ + + Tidak ada data + +
+
+
+
+
+
+ + {/* Information Text */} + + Transaksi yang sudah dipilih, tambah transaksi baru sesuai kebutuhan. + + + {/* Table */} + + + + + + + + Detil + Kirim + Terima + + + + + +
+ + + Tidak ada data + +
+
+
+
+
+
+
+ + {/* Summary Section */} + + + Total transaksi di Kledo harus sama dengan total mutasi + + + + + + Total transaksi di Kledo + + + + + 220.890 + + + + + + + + Total mutasi + + + + + 0 + + + + + + + + Selisih + + + + + 220.890 + + + + +
+ + {/* Transaction Summary */} + + + + + Detil + + {transactionData.detil} + + + + Kirim + + + {transactionData.kirim} + + + + + Terima + + + {transactionData.terima} + + + + + + + + Total + + + 220.890 + + + + + +
+
+ + {/* Footer Buttons */} + +
+ + +
+
+
+ ) +} + +export default CashBankDetailTransactionReconciliationDrawer From 5def10dc64c746c2751f7cdb2fbb913d9018228d Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 23:01:01 +0700 Subject: [PATCH 41/42] Account --- src/views/apps/account/AccountListTable.tsx | 2 +- src/views/apps/cash-bank/CashBankList.tsx | 124 ++++++++++++-------- 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/views/apps/account/AccountListTable.tsx b/src/views/apps/account/AccountListTable.tsx index 24b14a6..0df4835 100644 --- a/src/views/apps/account/AccountListTable.tsx +++ b/src/views/apps/account/AccountListTable.tsx @@ -65,7 +65,7 @@ type AccountTypeWithAction = AccountType & { } // Dummy Account Data -const accountsData: AccountType[] = [ +export const accountsData: AccountType[] = [ { id: 1, code: '1-10001', diff --git a/src/views/apps/cash-bank/CashBankList.tsx b/src/views/apps/cash-bank/CashBankList.tsx index 85c5517..92aa8f5 100644 --- a/src/views/apps/cash-bank/CashBankList.tsx +++ b/src/views/apps/cash-bank/CashBankList.tsx @@ -16,6 +16,9 @@ import CustomTextField from '@/@core/components/mui/TextField' import { getLocalizedUrl } from '@/utils/i18n' import { Locale } from '@/configs/i18n' import { useParams } from 'next/navigation' +import AccountFormDrawer, { AccountType } from '../account/AccountFormDrawer' +import { accountsData } from '../account/AccountListTable' +import { Button } from '@mui/material' // Types interface BankAccount { @@ -250,10 +253,15 @@ const DebouncedInput = ({ } const CashBankList = () => { const [searchQuery, setSearchQuery] = useState('') + const [editingAccount, setEditingAccount] = useState(null) + const [addAccountOpen, setAddAccountOpen] = useState(false) + const [data, setData] = useState(accountsData) const { lang: locale } = useParams() - // Handle button clicks - const handleAccountAction = () => {} + const handleCloseDrawer = () => { + setAddAccountOpen(false) + setEditingAccount(null) + } // Filter and search logic const filteredAccounts = useMemo(() => { @@ -267,60 +275,80 @@ const CashBankList = () => { }, [searchQuery]) return ( - - {/* Search and Filters */} - - - + <> + + {/* Search and Filters */} + +
setSearchQuery(value as string)} placeholder='Cari ' className='max-sm:is-full' /> - + + + +
+
+ + {/* Account Cards */} + + {filteredAccounts.length > 0 ? ( + filteredAccounts.map(account => ( + + + + )) + ) : ( + + + + Tidak ada akun yang ditemukan + + + Coba ubah kata kunci pencarian atau filter yang digunakan + + + + )}
- - {/* Account Cards */} - - {filteredAccounts.length > 0 ? ( - filteredAccounts.map(account => ( - - - - )) - ) : ( - - - - Tidak ada akun yang ditemukan - - - Coba ubah kata kunci pencarian atau filter yang digunakan - - - - )} - -
+ + ) } From a019a4bdbc9dfa2f7e5859bdb47284bf2ed233b1 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 11 Sep 2025 23:06:20 +0700 Subject: [PATCH 42/42] Update Expanse Detail Send Payment --- .../detail/ExpenseDetailSendPayment.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx index 8f2ac20..9b73635 100644 --- a/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx +++ b/src/views/apps/expense/detail/ExpenseDetailSendPayment.tsx @@ -17,6 +17,7 @@ import { import Grid from '@mui/material/Grid2' import CustomTextField from '@/@core/components/mui/TextField' import CustomAutocomplete from '@/@core/components/mui/Autocomplete' +import ImageUpload from '@/components/ImageUpload' interface PaymentFormData { totalDibayar: string @@ -114,6 +115,15 @@ const ExpenseDetailSendPayment: React.FC = () => { return new Intl.NumberFormat('id-ID').format(numAmount) } + const handleUpload = async (file: File): Promise => { + // Simulate upload + return new Promise(resolve => { + setTimeout(() => { + resolve(URL.createObjectURL(file)) + }, 1000) + }) + } + return ( { - - Drag and drop files here or click to upload - +