use App\Models\Customer; use App\Models\Item; use App\Models\Quotation; use App\Models\SalesInvoice; use App\Support\MoneyToWords; use Filament\Forms; use Filament\Forms\Form; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Get; use Filament\Forms\Set; public static function form(Form $form): Form { return $form ->schema([ Section::make('Invoice Info') ->schema([ TextInput::make('invoice_no') ->label('Invoice No.') ->disabled() ->dehydrated(false), DatePicker::make('invoice_date') ->label('Invoice Date') ->default(now()) ->required(), Select::make('customer_id') ->label('Customer') ->relationship('customer', 'name') ->searchable() ->preload() ->required(), Select::make('quotation_id') ->label('Quote No.') ->relationship('quotation', 'quotation_no') ->searchable() ->preload() ->reactive() ->afterStateUpdated(function (?string $state, Set $set, Get $get) { if (! $state) { return; } $quotation = Quotation::with(['customer', 'items'])->find($state); if (! $quotation) { return; } // Link quote header values $set('quote_no', $quotation->quotation_no); $set('customer_id', $quotation->customer_id); // Optional: pre-fill bill to from customer if ($quotation->customer) { $set('bill_to_name', $quotation->customer->name); $set('bill_to_address', $quotation->customer->address ?? null); } // Map quotation items into invoice lines $lines = $quotation->items->map(function ($item) { return [ 'item_id' => $item->item_id, 'description' => $item->description, 'qty' => $item->qty, 'unit_price' => $item->unit_price, 'discount' => $item->discount, 'line_total' => $item->line_total, ]; })->toArray(); $set('lines', $lines); // Copy totals; you can still edit them later $set('subtotal', $quotation->subtotal); $set('discount', $quotation->discount_total); $set('sst_amount', $quotation->tax_total); $set('total', $quotation->grand_total); // Reset deposit / balance if needed $set('deposit', 0); $set('balance_due', $quotation->grand_total); }), TextInput::make('customer_po_no') ->label('Cust. PO No.') ->maxLength(255), TextInput::make('status') ->default('Draft') ->maxLength(50), ]) ->columns(3), Section::make('Bill To') ->schema([ TextInput::make('bill_to_name') ->label('Bill To Name') ->maxLength(255), Textarea::make('bill_to_address') ->label('Bill To Address') ->rows(3), TextInput::make('pic') ->label('P.I.C.') ->maxLength(255), TextInput::make('ref_no') ->label('Our Ref / Your Ref') ->maxLength(255), TextInput::make('terms') ->label('Terms') ->maxLength(255), ]) ->columns(2), Section::make('Items') ->schema([ Repeater::make('lines') ->relationship() // uses SalesInvoice::lines() ->schema([ Select::make('item_id') ->label('Item') ->relationship('item', 'name') ->searchable() ->preload() ->reactive() ->afterStateUpdated(function (?string $state, Set $set) { if (! $state) { return; } $item = Item::find($state); if ($item) { $set('unit_price', $item->selling_price ?? 0); $set('description', $item->description ?? $item->name); } }) ->required(), TextInput::make('description') ->maxLength(255), TextInput::make('qty') ->numeric() ->default(1) ->reactive(), TextInput::make('unit_price') ->numeric() ->default(0) ->reactive(), TextInput::make('discount') ->numeric() ->default(0) ->reactive(), TextInput::make('line_total') ->label('Line Total') ->numeric() ->disabled() ->dehydrated() ->reactive() ->afterStateHydrated(function ($state, Set $set, Get $get) { $qty = (float) ($get('qty') ?? 0); $price = (float) ($get('unit_price') ?? 0); $discount = (float) ($get('discount') ?? 0); $set('line_total', ($qty * $price) - $discount); }), ]) ->columns(6) ->live() ->afterStateUpdated(function (Get $get, Set $set) { $lines = $get('lines') ?? []; $subtotal = 0; foreach ($lines as $line) { $subtotal += (float) ($line['line_total'] ?? 0); } $set('subtotal', $subtotal); // Discount is header-level; leave as-is unless you want auto $discount = (float) ($get('discount') ?? 0); $shipping = (float) ($get('shipping') ?? 0); $rounding = (float) ($get('rounding') ?? 0); // SST as percentage of subtotal - discount $sstRate = (float) ($get('sst_rate') ?? 0); $sstAmount = (($subtotal - $discount) * $sstRate / 100); $set('sst_amount', $sstAmount); $total = $subtotal - $discount + $shipping + $rounding + $sstAmount; $set('total', $total); $deposit = (float) ($get('deposit') ?? 0); $set('balance_due', $total - $deposit); }), ]), Section::make('Totals') ->schema([ TextInput::make('subtotal') ->numeric() ->disabled(), TextInput::make('discount') ->numeric() ->reactive() ->afterStateUpdated(function (Set $set, Get $get) { $subtotal = (float) ($get('subtotal') ?? 0); $discount = (float) ($get('discount') ?? 0); $shipping = (float) ($get('shipping') ?? 0); $rounding = (float) ($get('rounding') ?? 0); $sstRate = (float) ($get('sst_rate') ?? 0); $sstAmount = (($subtotal - $discount) * $sstRate / 100); $set('sst_amount', $sstAmount); $total = $subtotal - $discount + $shipping + $rounding + $sstAmount; $set('total', $total); $deposit = (float) ($get('deposit') ?? 0); $set('balance_due', $total - $deposit); }), TextInput::make('shipping') ->numeric() ->default(0) ->reactive() ->afterStateUpdated(fn (Set $set, Get $get) => self::recalculateTotals($set, $get) ), TextInput::make('rounding') ->numeric() ->default(0) ->reactive() ->afterStateUpdated(fn (Set $set, Get $get) => self::recalculateTotals($set, $get) ), TextInput::make('sst_rate') ->label('SST %') ->numeric() ->default(0) ->reactive() ->afterStateUpdated(fn (Set $set, Get $get) => self::recalculateTotals($set, $get) ), TextInput::make('sst_amount') ->numeric() ->disabled(), TextInput::make('total') ->numeric() ->disabled(), TextInput::make('deposit') ->numeric() ->default(0) ->reactive() ->afterStateUpdated(function (Set $set, Get $get) { $total = (float) ($get('total') ?? 0); $deposit = (float) ($get('deposit') ?? 0); $set('balance_due', $total - $deposit); }), TextInput::make('balance_due') ->numeric() ->disabled(), Placeholder::make('amount_in_words') ->label('Amount in Words') ->content(function (?SalesInvoice $record, Get $get) { $balance = $record?->balance_due ?? (float) ($get('balance_due') ?? 0); if ($balance <= 0) { return ''; } return MoneyToWords::toWords($balance); }), ]) ->columns(3), Section::make('Notes') ->schema([ Textarea::make('notes') ->rows(3), ]), ]); } /** * Helper to recalc totals (used in closures) */ protected static function recalculateTotals(Set $set, Get $get): void { $subtotal = (float) ($get('subtotal') ?? 0); $discount = (float) ($get('discount') ?? 0); $shipping = (float) ($get('shipping') ?? 0); $rounding = (float) ($get('rounding') ?? 0); $sstRate = (float) ($get('sst_rate') ?? 0); $sstAmount = (($subtotal - $discount) * $sstRate / 100); $set('sst_amount', $sstAmount); $total = $subtotal - $discount + $shipping + $rounding + $sstAmount; $set('total', $total); $deposit = (float) ($get('deposit') ?? 0); $set('balance_due', $total - $deposit); } (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); // node_modules/nprogress/nprogress.js var require_nprogress = __commonJS({ "node_modules/nprogress/nprogress.js"(exports, module) { (function(root, factory) { if (typeof define === "function" && define.amd) { define(factory); } else if (typeof exports === "object") { module.exports = factory(); } else { root.NProgress = factory(); } })(exports, function() { var NProgress2 = {}; NProgress2.version = "0.2.0"; var Settings = NProgress2.settings = { minimum: 0.08, easing: "ease", positionUsing: "", speed: 200, trickle: true, trickleRate: 0.02, trickleSpeed: 800, showSpinner: true, barSelector: '[role="bar"]', spinnerSelector: '[role="spinner"]', parent: "body", template: '
' }; NProgress2.configure = function(options) { var key, value; for (key in options) { value = options[key]; if (value !== void 0 && options.hasOwnProperty(key)) Settings[key] = value; } return this; }; NProgress2.status = null; NProgress2.set = function(n) { var started2 = NProgress2.isStarted(); n = clamp2(n, Settings.minimum, 1); NProgress2.status = n === 1 ? null : n; var progress = NProgress2.render(!started2), bar = progress.querySelector(Settings.barSelector), speed = Settings.speed, ease = Settings.easing; progress.offsetWidth; queue2(function(next) { if (Settings.positionUsing === "") Settings.positionUsing = NProgress2.getPositioningCSS(); css(bar, barPositionCSS(n, speed, ease)); if (n === 1) { css(progress, { transition: "none", opacity: 1 }); progress.offsetWidth; setTimeout(function() { css(progress, { transition: "all " + speed + "ms linear", opacity: 0 }); setTimeout(function() { NProgress2.remove(); next(); }, speed); }, speed); } else { setTimeout(next, speed); } }); return this; }; NProgress2.isStarted = function() { return typeof NProgress2.status === "number"; }; NProgress2.start = function() { if (!NProgress2.status) NProgress2.set(0); var work = function() { setTimeout(function() { if (!NProgress2.status) return; NProgress2.trickle(); work(); }, Settings.trickleSpeed); }; if (Settings.trickle) work(); return this; }; NProgress2.done = function(force) { if (!force && !NProgress2.status) return this; return NProgress2.inc(0.3 + 0.5 * Math.random()).set(1); }; NProgress2.inc = function(amount) { var n = NProgress2.status; if (!n) { return NProgress2.start(); } else { if (typeof amount !== "number") { amount = (1 - n) * clamp2(Math.random() * n, 0.1, 0.95); } n = clamp2(n + amount, 0, 0.994); return NProgress2.set(n); } }; NProgress2.trickle = function() { return NProgress2.inc(Math.random() * Settings.trickleRate); }; (function() { var initial = 0, current = 0; NProgress2.promise = function($promise) { if (!$promise || $promise.state() === "resolved") { return this; } if (current === 0) { NProgress2.start(); } initial++; current++; $promise.always(function() { current--; if (current === 0) { initial = 0; NProgress2.done(); } else { NProgress2.set((initial - current) / initial); } }); return this; }; })(); NProgress2.render = function(fromStart) { if (NProgress2.isRendered()) return document.getElementById("nprogress"); addClass(document.documentElement, "nprogress-busy"); var progress = document.createElement("div"); progress.id = "nprogress"; progress.innerHTML = Settings.template; var bar = progress.querySelector(Settings.barSelector), perc = fromStart ? "-100" : toBarPerc(NProgress2.status || 0), parent = document.querySelector(Settings.parent), spinner; css(bar, { transition: "all 0 linear", transform: "translate3d(" + perc + "%,0,0)" }); if (!Settings.showSpinner) { spinner = progress.querySelector(Settings.spinnerSelector); spinner && removeElement(spinner); } if (parent != document.body) { addClass(parent, "nprogress-custom-parent"); } parent.appendChild(progress); return progress; }; NProgress2.remove = function() { removeClass(document.documentElement, "nprogress-busy"); removeClass(document.querySelector(Settings.parent), "nprogress-custom-parent"); var progress = document.getElementById("nprogress"); progress && removeElement(progress); }; NProgress2.isRendered = function() { return !!document.getElementById("nprogress"); }; NProgress2.getPositioningCSS = function() { var bodyStyle = document.body.style; var vendorPrefix = "WebkitTransform" in bodyStyle ? "Webkit" : "MozTransform" in bodyStyle ? "Moz" : "msTransform" in bodyStyle ? "ms" : "OTransform" in bodyStyle ? "O" : ""; if (vendorPrefix + "Perspective" in bodyStyle) { return "translate3d"; } else if (vendorPrefix + "Transform" in bodyStyle) { return "translate"; } else { return "margin"; } }; function clamp2(n, min2, max2) { if (n < min2) return min2; if (n > max2) return max2; return n; } function toBarPerc(n) { return (-1 + n) * 100; } function barPositionCSS(n, speed, ease) { var barCSS; if (Settings.positionUsing === "translate3d") { barCSS = { transform: "translate3d(" + toBarPerc(n) + "%,0,0)" }; } else if (Settings.positionUsing === "translate") { barCSS = { transform: "translate(" + toBarPerc(n) + "%,0)" }; } else { barCSS = { "margin-left": toBarPerc(n) + "%" }; } barCSS.transition = "all " + speed + "ms " + ease; return barCSS; } var queue2 = function() { var pending = []; function next() { var fn = pending.shift(); if (fn) { fn(next); } } return function(fn) { pending.push(fn); if (pending.length == 1) next(); }; }(); var css = function() { var cssPrefixes = ["Webkit", "O", "Moz", "ms"], cssProps = {}; function camelCase3(string) { return string.replace(/^-ms-/, "ms-").replace(/-([\da-z])/gi, function(match, letter) { return letter.toUpperCase(); }); } function getVendorProp(name) { var style = document.body.style; if (name in style) return name; var i = cssPrefixes.length, capName = name.charAt(0).toUpperCase() + name.slice(1), vendorName; while (i--) { vendorName = cssPrefixes[i] + capName; if (vendorName in style) return vendorName; } return name; } function getStyleProp(name) { name = camelCase3(name); return cssProps[name] || (cssProps[name] = getVendorProp(name)); } function applyCss(element, prop, value) { prop = getStyleProp(prop); element.style[prop] = value; } return function(element, properties2) { var args = arguments, prop, value; if (args.length == 2) { for (prop in properties2) { value = properties2[prop]; if (value !== void 0 && properties2.hasOwnProperty(prop)) applyCss(element, prop, value); } } else { applyCss(element, args[1], args[2]); } }; }(); function hasClass(element, name) { var list = typeof element == "string" ? element : classList(element); return list.indexOf(" " + name + " ") >= 0; } function addClass(element, name) { var oldList = classList(element), newList = oldList + name; if (hasClass(oldList, name)) return; element.className = newList.substring(1); } function removeClass(element, name) { var oldList = classList(element), newList; if (!hasClass(element, name)) return; newList = oldList.replace(" " + name + " ", " "); element.className = newList.substring(1, newList.length - 1); } function classList(element) { return (" " + (element.className || "") + " ").replace(/\s+/gi, " "); } function removeElement(element) { element && element.parentNode && element.parentNode.removeChild(element); } return NProgress2; }); } }); // js/utils.js var Bag = class { constructor() { this.arrays = {}; } add(key, value) { if (!this.arrays[key]) this.arrays[key] = []; this.arrays[key].push(value); } remove(key) { if (this.arrays[key]) delete this.arrays[key]; } get(key) { return this.arrays[key] || []; } each(key, callback) { return this.get(key).forEach(callback); } }; var WeakBag = class { constructor() { this.arrays = /* @__PURE__ */ new WeakMap(); } add(key, value) { if (!this.arrays.has(key)) this.arrays.set(key, []); this.arrays.get(key).push(value); } remove(key) { if (this.arrays.has(key)) this.arrays.delete(key, []); } get(key) { return this.arrays.has(key) ? this.arrays.get(key) : []; } each(key, callback) { return this.get(key).forEach(callback); } }; function dispatch(target, name, detail = {}, bubbles = true) { target.dispatchEvent(new CustomEvent(name, { detail, bubbles, composed: true, cancelable: true })); } function listen(target, name, handler4) { target.addEventListener(name, handler4); return () => target.removeEventListener(name, handler4); } function isObjecty(subject) { return typeof subject === "object" && subject !== null; } function isObject(subject) { return isObjecty(subject) && !isArray(subject); } function isArray(subject) { return Array.isArray(subject); } function isFunction(subject) { return typeof subject === "function"; } function isPrimitive(subject) { return typeof subject !== "object" || subject === null; } function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } function dataGet(object, key) { if (key === "") return object; return key.split(".").reduce((carry, i) => { return carry?.[i]; }, object); } function dataSet(object, key, value) { let segments = key.split("."); if (segments.length === 1) { return object[key] = value; } let firstSegment = segments.shift(); let restOfSegments = segments.join("."); if (object[firstSegment] === void 0) { object[firstSegment] = {}; } dataSet(object[firstSegment], restOfSegments, value); } function diff(left, right, diffs = {}, path = "") { if (left === right) return diffs; if (typeof left !== typeof right || isObject(left) && isArray(right) || isArray(left) && isObject(right)) { diffs[path] = right; return diffs; } if (isPrimitive(left) || isPrimitive(right)) { diffs[path] = right; return diffs; } let leftKeys = Object.keys(left); Object.entries(right).forEach(([key, value]) => { diffs = { ...diffs, ...diff(left[key], right[key], diffs, path === "" ? key : `${path}.${key}`) }; leftKeys = leftKeys.filter((i) => i !== key); }); leftKeys.forEach((key) => { diffs[`${path}.${key}`] = "__rm__"; }); return diffs; } function extractData(payload) { let value = isSynthetic(payload) ? payload[0] : payload; let meta = isSynthetic(payload) ? payload[1] : void 0; if (isObjecty(value)) { Object.entries(value).forEach(([key, iValue]) => { value[key] = extractData(iValue); }); } return value; } function isSynthetic(subject) { return Array.isArray(subject) && subject.length === 2 && typeof subject[1] === "object" && Object.keys(subject[1]).includes("s"); } function getCsrfToken() { if (document.querySelector('meta[name="csrf-token"]')) { return document.querySelector('meta[name="csrf-token"]').getAttribute("content"); } if (document.querySelector("[data-csrf]")) { return document.querySelector("[data-csrf]").getAttribute("data-csrf"); } if (window.livewireScriptConfig["csrf"] ?? false) { return window.livewireScriptConfig["csrf"]; } throw "Livewire: No CSRF token detected"; } var nonce; function getNonce() { if (nonce) return nonce; if (window.livewireScriptConfig && (window.livewireScriptConfig["nonce"] ?? false)) { nonce = window.livewireScriptConfig["nonce"]; return nonce; } const elWithNonce = document.querySelector("style[data-livewire-style][nonce]"); if (elWithNonce) { nonce = elWithNonce.nonce; return nonce; } return null; } function getUpdateUri() { return document.querySelector("[data-update-uri]")?.getAttribute("data-update-uri") ?? window.livewireScriptConfig["uri"] ?? null; } function contentIsFromDump(content) { return !!content.match(/