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(/