wilddragon-site/scripts/patch_mobile_ticket.py

215 lines
7.7 KiB
Python

#!/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')