Add mobile ticket details patch script
This commit is contained in:
parent
6d901522d0
commit
521c6d67b6
1 changed files with 215 additions and 0 deletions
215
scripts/patch_mobile_ticket.py
Normal file
215
scripts/patch_mobile_ticket.py
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Patch MobileTicketAgent.vue to add BMG custom sections
|
||||||
|
(Log Time, Items Used, Customer Email + CC) to the Details tab.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TARGET = '/home/frappe/frappe-bench/frappe-bench/apps/helpdesk/desk/src/pages/ticket/MobileTicketAgent.vue'
|
||||||
|
|
||||||
|
with open(TARGET, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# ── 1. Template: inject custom sections after TicketAgentFields ──────────────
|
||||||
|
OLD_TMPL = ''' <TicketAgentFields
|
||||||
|
:ticket="ticket.data"
|
||||||
|
@update="({ field, value }) => updateTicket(field, value)"
|
||||||
|
class="!border-0"
|
||||||
|
/>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
NEW_TMPL = ''' <TicketAgentFields
|
||||||
|
:ticket="ticket.data"
|
||||||
|
@update="({ field, value }) => updateTicket(field, value)"
|
||||||
|
class="!border-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- BMG: Log Time -->
|
||||||
|
<div class="px-6 mt-3">
|
||||||
|
<Button variant="subtle" class="w-full" @click="promptLogTime">
|
||||||
|
<template #prefix><FeatherIcon name="clock" class="h-4 w-4" /></template>
|
||||||
|
Log Time
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BMG: Items Used -->
|
||||||
|
<div class="px-6 mt-3">
|
||||||
|
<label class="block text-xs text-gray-600 mb-1">Items Used</label>
|
||||||
|
<div v-if="mobileItemsList.length" class="space-y-1 mb-2">
|
||||||
|
<div v-for="it in mobileItemsList" :key="it.name" class="flex items-center justify-between text-xs bg-gray-50 rounded px-2 py-1">
|
||||||
|
<span class="truncate">{{ it.item_name || it.item || it.barcode || "\u2014" }}<span v-if="it.qty && it.qty != 1" class="text-gray-500"> \u00d7 {{ it.qty }}</span></span>
|
||||||
|
<Button icon="x" variant="ghost" size="sm" @click="mobileRemoveItem(it.name)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<input v-model="mobileNewItem" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="Item name or code\u2026" />
|
||||||
|
<input v-model="mobileNewBarcode" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="Barcode (optional)" />
|
||||||
|
<Button variant="subtle" class="w-full" :disabled="!mobileNewItem && !mobileNewBarcode" :loading="mobileAddingItem" @click="mobileAddItem">Add Item</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BMG: Customer Email + CC -->
|
||||||
|
<div class="px-6 mt-3 space-y-2 pb-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-1">Customer Email</label>
|
||||||
|
<input v-model="mobileRaisedBy" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="customer@example.com" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-1">CC</label>
|
||||||
|
<input v-model="mobileCcCsv" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="cc1@example.com, cc2@example.com" />
|
||||||
|
</div>
|
||||||
|
<Button variant="solid" class="w-full" :loading="mobileSavingRecipients" @click="mobileSaveRecipients">Save Recipients</Button>
|
||||||
|
</div>
|
||||||
|
</div>'''
|
||||||
|
|
||||||
|
assert OLD_TMPL in content, 'ERROR: template anchor not found'
|
||||||
|
content = content.replace(OLD_TMPL, NEW_TMPL, 1)
|
||||||
|
|
||||||
|
# ── 2. Add Button + FeatherIcon to frappe-ui imports ────────────────────────
|
||||||
|
OLD_IMPORTS = '''import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Dialog,
|
||||||
|
Dropdown,
|
||||||
|
FormControl,
|
||||||
|
Tabs,
|
||||||
|
call,
|
||||||
|
createResource,
|
||||||
|
toast,
|
||||||
|
} from "frappe-ui";'''
|
||||||
|
|
||||||
|
NEW_IMPORTS = '''import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Dropdown,
|
||||||
|
FeatherIcon,
|
||||||
|
FormControl,
|
||||||
|
Tabs,
|
||||||
|
call,
|
||||||
|
createResource,
|
||||||
|
toast,
|
||||||
|
} from "frappe-ui";'''
|
||||||
|
|
||||||
|
assert OLD_IMPORTS in content, 'ERROR: imports anchor not found'
|
||||||
|
content = content.replace(OLD_IMPORTS, NEW_IMPORTS, 1)
|
||||||
|
|
||||||
|
# ── 3. Initialize mobile refs in onSuccess ───────────────────────────────────
|
||||||
|
OLD_ONSUCCESS = ''' onSuccess: (data) => {
|
||||||
|
subjectInput.value = ticket.subject;
|
||||||
|
setupCustomizations(ticket, {'''
|
||||||
|
|
||||||
|
NEW_ONSUCCESS = ''' onSuccess: (data) => {
|
||||||
|
subjectInput.value = ticket.subject;
|
||||||
|
mobileRaisedBy.value = data.raised_by || "";
|
||||||
|
mobileCcCsv.value = data.cc_csv || "";
|
||||||
|
setupCustomizations(ticket, {'''
|
||||||
|
|
||||||
|
if OLD_ONSUCCESS in content:
|
||||||
|
content = content.replace(OLD_ONSUCCESS, NEW_ONSUCCESS, 1)
|
||||||
|
else:
|
||||||
|
print('WARNING: onSuccess anchor not found, skipping init')
|
||||||
|
|
||||||
|
# ── 4. Inject BMG reactive state + functions before </script> ────────────────
|
||||||
|
BMG_SCRIPT = '''
|
||||||
|
// ── BMG mobile additions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Log Time
|
||||||
|
async function promptLogTime() {
|
||||||
|
const hoursStr = window.prompt(
|
||||||
|
"Hours spent on this ticket:\\n\\nEnter a decimal like 0.5, 1, 2.5.",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
if (hoursStr === null) return;
|
||||||
|
const trimmed = (hoursStr || "").trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const hoursNum = parseFloat(trimmed);
|
||||||
|
if (isNaN(hoursNum) || hoursNum <= 0 || hoursNum > 24) {
|
||||||
|
window.alert("Please enter a number between 0 and 24.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notes = window.prompt("Notes (optional):", "") || "";
|
||||||
|
try {
|
||||||
|
await call("helpdesk_log_time_on_ticket", {
|
||||||
|
ticket: ticket.data.name,
|
||||||
|
hours: trimmed,
|
||||||
|
notes,
|
||||||
|
});
|
||||||
|
toast.success("Logged " + hoursNum + "h on ticket #" + ticket.data.name);
|
||||||
|
} catch (e) {
|
||||||
|
window.alert("Log time failed: " + ((e && e.message) || String(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items Used
|
||||||
|
const mobileNewItem = ref("");
|
||||||
|
const mobileNewBarcode = ref("");
|
||||||
|
const mobileAddingItem = ref(false);
|
||||||
|
const mobileItemsList = computed(() => ticket.data?.items_used || []);
|
||||||
|
|
||||||
|
async function mobileAddItem() {
|
||||||
|
if (!mobileNewItem.value && !mobileNewBarcode.value) return;
|
||||||
|
mobileAddingItem.value = true;
|
||||||
|
try {
|
||||||
|
await call("helpdesk_add_ticket_item", {
|
||||||
|
ticket: ticket.data.name,
|
||||||
|
item: mobileNewItem.value,
|
||||||
|
barcode: mobileNewBarcode.value,
|
||||||
|
});
|
||||||
|
ticket.reload();
|
||||||
|
mobileNewItem.value = "";
|
||||||
|
mobileNewBarcode.value = "";
|
||||||
|
toast.success("Item added");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Add failed: " + ((e && e.message) || String(e)));
|
||||||
|
} finally {
|
||||||
|
mobileAddingItem.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mobileRemoveItem(rowName) {
|
||||||
|
try {
|
||||||
|
await call("helpdesk_remove_ticket_item", {
|
||||||
|
ticket: ticket.data.name,
|
||||||
|
row: rowName,
|
||||||
|
});
|
||||||
|
ticket.reload();
|
||||||
|
toast.success("Item removed");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Remove failed: " + ((e && e.message) || String(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipients
|
||||||
|
const mobileRaisedBy = ref("");
|
||||||
|
const mobileCcCsv = ref("");
|
||||||
|
const mobileSavingRecipients = ref(false);
|
||||||
|
|
||||||
|
async function mobileSaveRecipients() {
|
||||||
|
mobileSavingRecipients.value = true;
|
||||||
|
try {
|
||||||
|
const ccArray = (mobileCcCsv.value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0)
|
||||||
|
.map((email) => ({ email_id: email }));
|
||||||
|
await call("helpdesk_update_ticket_recipients", {
|
||||||
|
ticket: ticket.data.name,
|
||||||
|
raised_by: (mobileRaisedBy.value || "").trim(),
|
||||||
|
cc: JSON.stringify(ccArray),
|
||||||
|
});
|
||||||
|
ticket.reload();
|
||||||
|
toast.success("Recipients saved");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Save failed: " + ((e && e.message) || String(e)));
|
||||||
|
} finally {
|
||||||
|
mobileSavingRecipients.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
assert '</script>' in content, 'ERROR: </script> not found'
|
||||||
|
content = content.replace('</script>', BMG_SCRIPT + '</script>', 1)
|
||||||
|
|
||||||
|
with open(TARGET, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print('PATCH OK')
|
||||||
Loading…
Reference in a new issue