Added Job Modal.
This commit is contained in:
parent
721b50d058
commit
20b3c1166f
@ -160,6 +160,17 @@ def get_clients(options):
|
||||
"data": tableRows if options["for_table"] else clients
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upsert_estimate(data):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upsert_job(data):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upsert_client(data):
|
||||
data = json.loads(data)
|
||||
@ -330,10 +341,10 @@ def get_warranty_claims(options):
|
||||
}
|
||||
options = {**defaultOptions, **options}
|
||||
print("DEBUG: Final warranty options:", options)
|
||||
|
||||
|
||||
warranties = []
|
||||
tableRows = []
|
||||
|
||||
|
||||
# Map frontend field names to backend field names for Warranty Claim doctype
|
||||
def map_warranty_field_name(frontend_field):
|
||||
field_mapping = {
|
||||
@ -350,7 +361,7 @@ def get_warranty_claims(options):
|
||||
"warrantyStatus": "warranty_amc_status"
|
||||
}
|
||||
return field_mapping.get(frontend_field, frontend_field)
|
||||
|
||||
|
||||
# Process filters from PrimeVue format to Frappe format
|
||||
processed_filters = {}
|
||||
if options["filters"]:
|
||||
@ -359,12 +370,12 @@ def get_warranty_claims(options):
|
||||
if filter_obj["value"] is not None and filter_obj["value"] != "":
|
||||
# Map frontend field names to backend field names
|
||||
backend_field = map_warranty_field_name(field_name)
|
||||
|
||||
|
||||
# Handle different match modes
|
||||
match_mode = filter_obj.get("matchMode", "contains")
|
||||
if isinstance(match_mode, str):
|
||||
match_mode = match_mode.lower()
|
||||
|
||||
|
||||
if match_mode in ("contains", "contains"):
|
||||
processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"]
|
||||
elif match_mode in ("startswith", "startsWith"):
|
||||
@ -376,7 +387,7 @@ def get_warranty_claims(options):
|
||||
else:
|
||||
# Default to contains
|
||||
processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"]
|
||||
|
||||
|
||||
# Process sorting
|
||||
order_by = None
|
||||
if options.get("sorting") and options["sorting"]:
|
||||
@ -390,31 +401,31 @@ def get_warranty_claims(options):
|
||||
# Map frontend field to backend field
|
||||
backend_sort_field = map_warranty_field_name(sort_field)
|
||||
order_by = f"{backend_sort_field} {sort_direction}"
|
||||
|
||||
|
||||
print("DEBUG: Processed warranty filters:", processed_filters)
|
||||
print("DEBUG: Warranty order by:", order_by)
|
||||
|
||||
|
||||
count = frappe.db.count("Warranty Claim", filters=processed_filters)
|
||||
print("DEBUG: Total warranty claims count:", count)
|
||||
|
||||
|
||||
warranty_claims = frappe.db.get_all(
|
||||
"Warranty Claim",
|
||||
fields=options["fields"],
|
||||
"Warranty Claim",
|
||||
fields=options["fields"],
|
||||
filters=processed_filters,
|
||||
limit=options["page_size"],
|
||||
start=(options["page"] - 1) * options["page_size"],
|
||||
order_by=order_by
|
||||
)
|
||||
|
||||
|
||||
for warranty in warranty_claims:
|
||||
warranty_obj = {}
|
||||
tableRow = {}
|
||||
|
||||
|
||||
tableRow["id"] = warranty["name"]
|
||||
tableRow["warrantyId"] = warranty["name"]
|
||||
tableRow["customer"] = warranty.get("customer_name", "")
|
||||
tableRow["serviceAddress"] = warranty.get("service_address", warranty.get("address_display", ""))
|
||||
|
||||
|
||||
# Extract a brief description from the complaint HTML
|
||||
complaint_text = warranty.get("complaint", "")
|
||||
if complaint_text:
|
||||
@ -426,7 +437,7 @@ def get_warranty_claims(options):
|
||||
tableRow["issueDescription"] = clean_text
|
||||
else:
|
||||
tableRow["issueDescription"] = ""
|
||||
|
||||
|
||||
tableRow["status"] = warranty.get("status", "")
|
||||
tableRow["complaintDate"] = warranty.get("complaint_date", "")
|
||||
tableRow["complaintRaisedBy"] = warranty.get("complaint_raised_by", "")
|
||||
@ -434,7 +445,7 @@ def get_warranty_claims(options):
|
||||
tableRow["territory"] = warranty.get("territory", "")
|
||||
tableRow["resolutionDate"] = warranty.get("resolution_date", "")
|
||||
tableRow["warrantyStatus"] = warranty.get("warranty_amc_status", "")
|
||||
|
||||
|
||||
# Add priority based on status and date (can be customized)
|
||||
if warranty.get("status") == "Open":
|
||||
# Calculate priority based on complaint date
|
||||
@ -444,7 +455,7 @@ def get_warranty_claims(options):
|
||||
complaint_date = datetime.strptime(complaint_date, "%Y-%m-%d").date()
|
||||
elif isinstance(complaint_date, datetime):
|
||||
complaint_date = complaint_date.date()
|
||||
|
||||
|
||||
days_old = (date.today() - complaint_date).days
|
||||
if days_old > 7:
|
||||
tableRow["priority"] = "High"
|
||||
@ -456,12 +467,12 @@ def get_warranty_claims(options):
|
||||
tableRow["priority"] = "Medium"
|
||||
else:
|
||||
tableRow["priority"] = "Low"
|
||||
|
||||
|
||||
tableRows.append(tableRow)
|
||||
|
||||
|
||||
warranty_obj["warranty_claim"] = warranty
|
||||
warranties.append(warranty_obj)
|
||||
|
||||
|
||||
return {
|
||||
"pagination": {
|
||||
"total": count,
|
||||
@ -470,4 +481,4 @@ def get_warranty_claims(options):
|
||||
"total_pages": (count + options["page_size"] - 1) // options["page_size"]
|
||||
},
|
||||
"data": tableRows if options["for_table"] else warranties
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { IconoirProvider } from "@iconoir/vue";
|
||||
import SideBar from "./components/SideBar.vue";
|
||||
import CreateClientModal from "./components/modals/CreateClientModal.vue";
|
||||
import CreateEstimateModal from "./components/modals/CreateEstimateModal.vue";
|
||||
import CreateJobModal from "./components/modals/CreateJobModal.vue";
|
||||
import GlobalLoadingOverlay from "./components/common/GlobalLoadingOverlay.vue";
|
||||
import ScrollPanel from "primevue/scrollpanel";
|
||||
</script>
|
||||
@ -28,6 +29,7 @@ import ScrollPanel from "primevue/scrollpanel";
|
||||
<!-- Global Modals -->
|
||||
<CreateClientModal />
|
||||
<CreateEstimateModal />
|
||||
<CreateJobModal />
|
||||
|
||||
<!-- Global Loading Overlay -->
|
||||
<GlobalLoadingOverlay />
|
||||
|
||||
@ -4,6 +4,7 @@ const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us";
|
||||
const FRAPPE_PROXY_METHOD = "custom_ui.api.proxy.request";
|
||||
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.upsert_client";
|
||||
const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.upsert_estimate";
|
||||
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.upsert_job";
|
||||
|
||||
class Api {
|
||||
static async request(frappeMethod, args = {}) {
|
||||
@ -417,6 +418,13 @@ class Api {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async createJob(jobData) {
|
||||
const payload = DataUtils.toSnakeCaseObject(jobData);
|
||||
const result = await this.request(FRAPPE_UPSERT_JOB_METHOD, { data: payload });
|
||||
console.log("DEBUG: API - Created Job: ", result);
|
||||
return result
|
||||
}
|
||||
|
||||
// External API calls
|
||||
|
||||
/**
|
||||
|
||||
@ -40,7 +40,9 @@ const createButtons = ref([
|
||||
{
|
||||
label: "Job",
|
||||
command: () => {
|
||||
frappe.new_doc("Job");
|
||||
//frappe.new_doc("Job");
|
||||
console.log("New Job");
|
||||
modalStore.openModal("createJob");
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
324
frontend/src/components/modals/CreateJobModal.vue
Normal file
324
frontend/src/components/modals/CreateJobModal.vue
Normal file
@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<Modal
|
||||
:visible="isVisible"
|
||||
:options="modalOptions"
|
||||
@update:visible="handleVisibilityChange"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #title> Create New Job </template>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div v-if="statusMessage" class="status-message" :class="`status-${statusType}`">
|
||||
<i :class="getStatusIcon(statusType)" class="status-icon"></i>
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
ref="formRef"
|
||||
:fields="formFields"
|
||||
:form-data="formData"
|
||||
:show-cancel-button="true"
|
||||
:validate-on-change="false"
|
||||
:validate-on-blur="true"
|
||||
:validate-on-submit="true"
|
||||
:loading="isSubmitting"
|
||||
:disable-on-loading="true"
|
||||
submit-button-text="Create Job"
|
||||
cancel-button-text="Cancel"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from "vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Modal from "@/components/common/Modal.vue";
|
||||
import Form from "@/components/common/Form.vue";
|
||||
import Api from "@/api";
|
||||
import DataUtils from "../../utils";
|
||||
|
||||
const modalStore = useModalStore();
|
||||
|
||||
// Modal visibility computed property
|
||||
const isVisible = computed(() => modalStore.isModalOpen("createJob"));
|
||||
const customerNames = ref([]);
|
||||
const companyNames = ref([]);
|
||||
// Form reference for controlling its state
|
||||
const formRef = ref(null);
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
address: "",
|
||||
requireHalfDown: false,
|
||||
customerName: "",
|
||||
company: "",
|
||||
});
|
||||
|
||||
// Status message for user feedback
|
||||
const statusMessage = ref("");
|
||||
const statusType = ref("info"); // 'info', 'warning', 'error', 'success'
|
||||
|
||||
// Modal configuration
|
||||
const modalOptions = {
|
||||
maxWidth: "600px",
|
||||
persistent: false,
|
||||
showActions: false,
|
||||
title: "Create New Job",
|
||||
overlayColor: "rgb(59, 130, 246)", // Blue background
|
||||
overlayOpacity: 0.8,
|
||||
cardClass: "create-job-modal",
|
||||
closeOnOutsideClick: true,
|
||||
closeOnEscape: true,
|
||||
};
|
||||
|
||||
// Form field definitions
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
name: "address",
|
||||
label: "Installation Address",
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "Enter street address",
|
||||
helpText: "Street address for the installation service.",
|
||||
cols: 12,
|
||||
md: 6,
|
||||
},
|
||||
{
|
||||
name: "requireHalfDown",
|
||||
label: "Requires Half Down?",
|
||||
type: "check",
|
||||
required: true,
|
||||
cols: 12,
|
||||
md: 6,
|
||||
helpText: "Check this box if the job requires half down to start.",
|
||||
},
|
||||
{
|
||||
name: "customerName",
|
||||
label: "Client Name",
|
||||
type: "autocomplete", // Changed from 'select' to 'autocomplete'
|
||||
required: true,
|
||||
placeholder: "Type or select client name",
|
||||
cols: 12,
|
||||
md: 6,
|
||||
options: customerNames.value, // Direct array of strings
|
||||
forceSelection: false, // Allow custom entries not in the list
|
||||
dropdown: true,
|
||||
// For string arrays, don't set optionLabel at all
|
||||
helpText: "Select an existing client or enter a new client name",
|
||||
// Let the Form component handle filtering automatically
|
||||
},
|
||||
{
|
||||
name: "company",
|
||||
label: "Company Name",
|
||||
type: "autocomplete", // Changed from 'select' to 'autocomplete'
|
||||
required: true,
|
||||
placeholder: "Select Company",
|
||||
cols: 12,
|
||||
md: 6,
|
||||
options: companyNames.value, // Direct array of strings
|
||||
dropdown: true,
|
||||
// For string arrays, don't set optionLabel at all
|
||||
helpText: "Select company associated with this job.",
|
||||
// Let the Form component handle filtering automatically
|
||||
},
|
||||
]);
|
||||
|
||||
// Show status message to user
|
||||
function showStatusMessage(message, type = "info") {
|
||||
statusMessage.value = message;
|
||||
statusType.value = type;
|
||||
|
||||
// Auto-clear message after 5 seconds
|
||||
setTimeout(() => {
|
||||
statusMessage.value = "";
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Get icon class for status messages
|
||||
function getStatusIcon(type) {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return "pi pi-exclamation-triangle";
|
||||
case "error":
|
||||
return "pi pi-times-circle";
|
||||
case "success":
|
||||
return "pi pi-check-circle";
|
||||
default:
|
||||
return "pi pi-info-circle";
|
||||
}
|
||||
}
|
||||
|
||||
// Submission state to prevent double submission
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// Handle form submission
|
||||
async function handleSubmit(formDataFromEvent) {
|
||||
// Prevent double submission with detailed logging
|
||||
if (isSubmitting.value) {
|
||||
console.warn(
|
||||
"CreateJobModal: Form submission already in progress, ignoring duplicate submission",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"CreateJobModal: Form submission started with data:",
|
||||
formDataFromEvent || formData,
|
||||
);
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
showStatusMessage("Creating job...", "info");
|
||||
|
||||
// Use the form data from the event if provided, otherwise use reactive formData
|
||||
const dataToSubmit = formDataFromEvent || formData;
|
||||
|
||||
console.log("CreateJobModal: Calling API with data:", dataToSubmit);
|
||||
|
||||
// Call API to create client
|
||||
const response = await Api.createJob(dataToSubmit);
|
||||
|
||||
console.log("CreateJobModal: API response received:", response);
|
||||
|
||||
if (response && response.success) {
|
||||
showStatusMessage("Job created successfully!", "success");
|
||||
|
||||
// Close modal after a brief delay
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(response?.message || "Failed to create job");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("CreateJobModal: Error creating job:", error);
|
||||
showStatusMessage(error.message || "Failed to create job. Please try again.", "error");
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
// Also reset the Form component's internal submission state
|
||||
if (formRef.value && formRef.value.stopLoading) {
|
||||
formRef.value.stopLoading();
|
||||
}
|
||||
console.log("CreateJobModal: Form submission completed, isSubmitting reset to false");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cancel action
|
||||
function handleCancel() {
|
||||
handleClose();
|
||||
}
|
||||
|
||||
// Handle modal close
|
||||
function handleClose() {
|
||||
modalStore.closeModal("createJob");
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// Handle visibility changes
|
||||
function handleVisibilityChange(visible) {
|
||||
if (!visible) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form data
|
||||
function resetForm() {
|
||||
Object.keys(formData).forEach((key) => {
|
||||
formData[key] = "";
|
||||
});
|
||||
availableCities.value = [];
|
||||
isLoadingZipcode.value = false;
|
||||
statusMessage.value = "";
|
||||
statusType.value = "info";
|
||||
}
|
||||
|
||||
// Initialize modal in store when component mounts
|
||||
modalStore.initializeModal("createJob", {
|
||||
closeOnEscape: true,
|
||||
closeOnOutsideClick: true,
|
||||
});
|
||||
|
||||
watch(isVisible, async () => {
|
||||
if (isVisible.value) {
|
||||
try {
|
||||
const names = await Api.getCustomerNames();
|
||||
customerNames.value = names;
|
||||
} catch (error) {
|
||||
console.error("Error loading customer names:", error);
|
||||
}
|
||||
try {
|
||||
const names = await Api.getCompanyNames();
|
||||
companyNames.value = names;
|
||||
} catch (error) {
|
||||
console.error("Error loading company names:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-client-modal {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Custom styling for the modal content */
|
||||
:deep(.modal-header) {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.modal-title) {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
:deep(.modal-close-btn) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:deep(.modal-content) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Status message styling */
|
||||
.status-message {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user