From ad28218893a255f1721a15d873e1f18240a79fac Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 28 Oct 2025 18:05:31 +0530 Subject: [PATCH] refactor: coupon list and form --- frappe-ui | 2 +- frontend/components.d.ts | 6 +- .../src/components/Settings/CouponDetails.vue | 230 ---------------- frontend/src/components/Settings/Coupons.vue | 144 ---------- .../Settings/Coupons/CouponDetails.vue | 245 ++++++++++++++++++ .../Settings/Coupons/CouponItems.vue | 164 ++++++++++++ .../Settings/Coupons/CouponList.vue | 203 +++++++++++++++ .../components/Settings/Coupons/Coupons.vue | 54 ++++ .../src/components/Settings/Coupons/types.ts | 30 +++ frontend/src/components/Settings/Settings.vue | 14 +- lms/lms/doctype/lms_coupon/lms_coupon.json | 112 ++++---- lms/lms/doctype/lms_coupon/lms_coupon.py | 71 +---- 12 files changed, 789 insertions(+), 486 deletions(-) delete mode 100644 frontend/src/components/Settings/CouponDetails.vue delete mode 100644 frontend/src/components/Settings/Coupons.vue create mode 100644 frontend/src/components/Settings/Coupons/CouponDetails.vue create mode 100644 frontend/src/components/Settings/Coupons/CouponItems.vue create mode 100644 frontend/src/components/Settings/Coupons/CouponList.vue create mode 100644 frontend/src/components/Settings/Coupons/Coupons.vue create mode 100644 frontend/src/components/Settings/Coupons/types.ts diff --git a/frappe-ui b/frappe-ui index 310089f4..f1bde9bc 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit 310089f4a40a6f6c9fa65d3e942c853e893df8ee +Subproject commit f1bde9bcb271af47e9f5de190a18dff8604f5312 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 3352de8f..3626a724 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -41,9 +41,11 @@ declare module 'vue' { CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default'] CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default'] ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default'] - CouponDetails: typeof import('./src/components/Settings/CouponDetails.vue')['default'] - Coupons: typeof import('./src/components/Settings/Coupons.vue')['default'] ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default'] + CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default'] + CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default'] + CouponList: typeof import('./src/components/Settings/Coupons/CouponList.vue')['default'] + Coupons: typeof import('./src/components/Settings/Coupons/Coupons.vue')['default'] CourseCard: typeof import('./src/components/CourseCard.vue')['default'] CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default'] CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default'] diff --git a/frontend/src/components/Settings/CouponDetails.vue b/frontend/src/components/Settings/CouponDetails.vue deleted file mode 100644 index 671d2f0c..00000000 --- a/frontend/src/components/Settings/CouponDetails.vue +++ /dev/null @@ -1,230 +0,0 @@ - - diff --git a/frontend/src/components/Settings/Coupons.vue b/frontend/src/components/Settings/Coupons.vue deleted file mode 100644 index eac012d5..00000000 --- a/frontend/src/components/Settings/Coupons.vue +++ /dev/null @@ -1,144 +0,0 @@ - - diff --git a/frontend/src/components/Settings/Coupons/CouponDetails.vue b/frontend/src/components/Settings/Coupons/CouponDetails.vue new file mode 100644 index 00000000..2ac9c4b4 --- /dev/null +++ b/frontend/src/components/Settings/Coupons/CouponDetails.vue @@ -0,0 +1,245 @@ + + diff --git a/frontend/src/components/Settings/Coupons/CouponItems.vue b/frontend/src/components/Settings/Coupons/CouponItems.vue new file mode 100644 index 00000000..e8f68f03 --- /dev/null +++ b/frontend/src/components/Settings/Coupons/CouponItems.vue @@ -0,0 +1,164 @@ + + diff --git a/frontend/src/components/Settings/Coupons/CouponList.vue b/frontend/src/components/Settings/Coupons/CouponList.vue new file mode 100644 index 00000000..d093167e --- /dev/null +++ b/frontend/src/components/Settings/Coupons/CouponList.vue @@ -0,0 +1,203 @@ + + diff --git a/frontend/src/components/Settings/Coupons/Coupons.vue b/frontend/src/components/Settings/Coupons/Coupons.vue new file mode 100644 index 00000000..77713d5b --- /dev/null +++ b/frontend/src/components/Settings/Coupons/Coupons.vue @@ -0,0 +1,54 @@ + + diff --git a/frontend/src/components/Settings/Coupons/types.ts b/frontend/src/components/Settings/Coupons/types.ts new file mode 100644 index 00000000..c4d67118 --- /dev/null +++ b/frontend/src/components/Settings/Coupons/types.ts @@ -0,0 +1,30 @@ +export interface Coupon { + name: string; + enabled: boolean; + code: string; + discount_type: 'Percentage' | 'Fixed Amount'; + percentage_discount?: number; + fixed_amount_discount?: number; + expires_on?: string; + description?: string; + usage_limit?: number; + redemptions_count: number; + applicable_items: ApplicableItem[]; +} + +export type ApplicableItem = { + reference_doctype: "LMS Course" | "LMS Batch"; + reference_name: string; + name: string; + parent: string; + parenttype: "LMS Coupon"; + parentfield: "applicable_items"; +} + +export interface Coupons { + data: Coupon[]; + update: (args: { filters: any[] }) => void; + insert: { submit: (params: Coupon, options: { onSuccess: (data: Coupon) => void; onError?: (err: any) => void }) => void }; + setValue: { submit: (params: Coupon, options: { onSuccess: (data: Coupon) => void; onError?: (err: any) => void }) => void }; + reload: () => void; +} \ No newline at end of file diff --git a/frontend/src/components/Settings/Settings.vue b/frontend/src/components/Settings/Settings.vue index 78c98a6b..35571494 100644 --- a/frontend/src/components/Settings/Settings.vue +++ b/frontend/src/components/Settings/Settings.vue @@ -78,7 +78,7 @@ import Categories from '@/components/Settings/Categories.vue' import EmailTemplates from '@/components/Settings/EmailTemplates.vue' import BrandSettings from '@/components/Settings/BrandSettings.vue' import PaymentGateways from '@/components/Settings/PaymentGateways.vue' -import Coupons from '@/components/Settings/Coupons.vue' +import Coupons from '@/components/Settings/Coupons/Coupons.vue' import Transactions from '@/components/Settings/Transactions.vue' import ZoomSettings from '@/components/Settings/ZoomSettings.vue' import Badges from '@/components/Settings/Badges.vue' @@ -228,18 +228,18 @@ const tabsStructure = computed(() => { template: markRaw(PaymentGateways), description: 'Add and manage all your payment gateways', }, - { - label: 'Coupons', - icon: 'Tag', - template: markRaw(Coupons), - description: 'Create and manage coupon codes', - }, { label: 'Transactions', icon: 'Landmark', template: markRaw(Transactions), description: 'View all your payment transactions', }, + { + label: 'Coupons', + icon: 'Ticket', + template: markRaw(Coupons), + description: 'Manage discount coupons for courses and batches', + }, ], }, { diff --git a/lms/lms/doctype/lms_coupon/lms_coupon.json b/lms/lms/doctype/lms_coupon/lms_coupon.json index 31eb826a..26dcb5a9 100644 --- a/lms/lms/doctype/lms_coupon/lms_coupon.json +++ b/lms/lms/doctype/lms_coupon/lms_coupon.json @@ -6,15 +6,19 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "enabled", + "section_break_spfj", "code", - "discount_type", - "percent_off", - "amount_off", - "active", "expires_on", + "column_break_mptc", + "discount_type", + "percentage_discount", + "fixed_amount_discount", + "section_break_ixxu", "usage_limit", - "times_redeemed", - "description", + "column_break_dcvj", + "redemption_count", + "section_break_ophm", "applicable_items" ], "fields": [ @@ -30,31 +34,11 @@ "fieldname": "discount_type", "fieldtype": "Select", "in_list_view": 1, + "in_standard_filter": 1, "label": "Discount Type", - "options": "Percent\nAmount", + "options": "Percentage\nFixed Amount", "reqd": 1 }, - { - "depends_on": "eval:doc.discount_type=='Percent'", - "fieldname": "percent_off", - "fieldtype": "Percent", - "label": "Percent Off", - "mandatory_depends_on": "eval:doc.discount_type=='Percent'" - }, - { - "depends_on": "eval:doc.discount_type=='Amount'", - "fieldname": "amount_off", - "fieldtype": "Int", - "label": "Amount Off", - "mandatory_depends_on": "eval:doc.discount_type=='Amount'" - }, - { - "default": "1", - "fieldname": "active", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Active" - }, { "fieldname": "expires_on", "fieldtype": "Date", @@ -64,20 +48,8 @@ { "fieldname": "usage_limit", "fieldtype": "Int", - "label": "Usage Limit" - }, - { - "default": "0", - "fieldname": "times_redeemed", - "fieldtype": "Int", "in_list_view": 1, - "label": "Times Redeemed", - "read_only": 1 - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Description" + "label": "Usage Limit" }, { "fieldname": "applicable_items", @@ -85,13 +57,64 @@ "label": "Applicable Items", "options": "LMS Coupon Item", "reqd": 1 + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Enabled" + }, + { + "fieldname": "column_break_mptc", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ixxu", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_dcvj", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ophm", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.discount_type=='Percentage'", + "fieldname": "percentage_discount", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Percentage Discount", + "mandatory_depends_on": "eval:doc.discount_type=='Percentage'" + }, + { + "depends_on": "eval:doc.discount_type=='Fixed Amount'", + "fieldname": "fixed_amount_discount", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Fixed Amount Discount", + "mandatory_depends_on": "eval:doc.discount_type=='Fixed Amount'" + }, + { + "default": "0", + "fieldname": "redemption_count", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Redemption Count", + "read_only": 1 + }, + { + "fieldname": "section_break_spfj", + "fieldtype": "Section Break" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-10-12 18:41:35.034875", - "modified_by": "Administrator", + "modified": "2025-10-27 19:52:11.835042", + "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Coupon", "naming_rule": "Random", @@ -162,5 +185,6 @@ "rows_threshold_for_grid_search": 20, "sort_field": "creation", "sort_order": "DESC", - "states": [] + "states": [], + "title_field": "code" } diff --git a/lms/lms/doctype/lms_coupon/lms_coupon.py b/lms/lms/doctype/lms_coupon/lms_coupon.py index d5328d84..1054692b 100644 --- a/lms/lms/doctype/lms_coupon/lms_coupon.py +++ b/lms/lms/doctype/lms_coupon/lms_coupon.py @@ -4,73 +4,28 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import nowdate +from frappe.utils import cint, nowdate class LMSCoupon(Document): def validate(self): + self.convert_to_uppercase() + self.validate_expiry_date() + self.validate_applicable_items() + self.validate_usage_limit() + + def convert_to_uppercase(self): if self.code: self.code = self.code.strip().upper() - if not self.code: - frappe.throw(_("Coupon code is required")) - - if len(self.code) < 6: - frappe.throw(_("Coupon code must be atleast 6 characters")) - - if self.name: - existing = frappe.db.exists( - "LMS Coupon", - { - "code": self.code, - "name": ["!=", self.name], - }, - ) - else: - existing = frappe.db.exists("LMS Coupon", {"code": self.code}) - - if existing: - frappe.throw(_("Coupon code is already taken. Use a different one")) - - if not self.discount_type: - frappe.throw(_("Discount type is required")) - - if self.discount_type == "Percent": - if not self.percent_off or self.percent_off == "": - frappe.throw(_("Discount percentage is required")) - try: - percent_value = float(self.percent_off) - if not (0 < percent_value <= 100): - frappe.throw(_("Discount percentage must be between 1 and 100")) - except (ValueError, TypeError): - frappe.throw(_("Discount percentage must be a valid number")) - self.amount_off = None - - if self.discount_type == "Amount": - if not self.amount_off or self.amount_off == "": - frappe.throw(_("Discount amount is required")) - try: - amount_value = float(self.amount_off) - if amount_value < 0: - frappe.throw(_("Discount amount cannot be negative")) - except (ValueError, TypeError): - frappe.throw(_("Discount amount must be a valid number")) - self.percent_off = None - - if self.usage_limit is not None and self.usage_limit != "": - try: - usage_value = int(self.usage_limit) - if usage_value < 0: - frappe.throw(_("Usage limit cannot be negative")) - except (ValueError, TypeError): - frappe.throw(_("Usage limit must be a valid number")) - + def validate_expiry_date(self): if self.expires_on and str(self.expires_on) < nowdate(): frappe.throw(_("Expiry date cannot be in the past")) + def validate_applicable_items(self): if not self.get("applicable_items") or len(self.get("applicable_items")) == 0: - frappe.throw(_("Please select atleast one course or batch")) + frappe.throw(_("At least one applicable item is required")) - for item in self.get("applicable_items"): - if not item.get("reference_name"): - frappe.throw(_("Please select a valid course or batch")) + def validate_usage_limit(self): + if self.usage_limit is not None and cint(self.usage_limit) < 0: + frappe.throw(_("Usage limit cannot be negative"))