From 5e46fa06f18f554cb77b38e539adb7dbdb630ad0 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Feb 2026 13:44:29 -0600 Subject: [PATCH] actual --- docker-compose.yml | 1 + prod_backup.sql | 321 +++++++++++ prod_data_only.sql | 102 ++++ server.js | 948 +++++++------------------------- templates/invoice-template.html | 270 +++++++++ templates/quote-template.html | 267 +++++++++ 6 files changed, 1156 insertions(+), 753 deletions(-) create mode 100644 prod_backup.sql create mode 100644 prod_data_only.sql create mode 100644 templates/invoice-template.html create mode 100644 templates/quote-template.html diff --git a/docker-compose.yml b/docker-compose.yml index b716772..0ca70c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,7 @@ services: DB_NAME: quotes_db volumes: - ./public/uploads:/app/public/uploads + - ./templates:/app/templates # NEU! depends_on: postgres: condition: service_healthy diff --git a/prod_backup.sql b/prod_backup.sql new file mode 100644 index 0000000..9ee1e7d --- /dev/null +++ b/prod_backup.sql @@ -0,0 +1,321 @@ +-- +-- PostgreSQL database dump +-- + +\restrict bxGU7dQ4DrNrHU2OuyEH16NHE6ZA8yFm2MADa6p2XI8qbowdWdtlaDeKSSp2NYx + +-- Dumped from database version 15.15 +-- Dumped by pg_dump version 15.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: customers; Type: TABLE; Schema: public; Owner: quoteuser +-- + +CREATE TABLE public.customers ( + id integer NOT NULL, + name character varying(255) NOT NULL, + street character varying(255) NOT NULL, + city character varying(100) NOT NULL, + state character varying(2) NOT NULL, + zip_code character varying(10) NOT NULL, + account_number character varying(50), + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.customers OWNER TO quoteuser; + +-- +-- Name: customers_id_seq; Type: SEQUENCE; Schema: public; Owner: quoteuser +-- + +CREATE SEQUENCE public.customers_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.customers_id_seq OWNER TO quoteuser; + +-- +-- Name: customers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quoteuser +-- + +ALTER SEQUENCE public.customers_id_seq OWNED BY public.customers.id; + + +-- +-- Name: quote_items; Type: TABLE; Schema: public; Owner: quoteuser +-- + +CREATE TABLE public.quote_items ( + id integer NOT NULL, + quote_id integer, + quantity character varying(20) NOT NULL, + description text NOT NULL, + rate character varying(50) NOT NULL, + amount character varying(50) NOT NULL, + is_tbd boolean DEFAULT false, + item_order integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.quote_items OWNER TO quoteuser; + +-- +-- Name: quote_items_id_seq; Type: SEQUENCE; Schema: public; Owner: quoteuser +-- + +CREATE SEQUENCE public.quote_items_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.quote_items_id_seq OWNER TO quoteuser; + +-- +-- Name: quote_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quoteuser +-- + +ALTER SEQUENCE public.quote_items_id_seq OWNED BY public.quote_items.id; + + +-- +-- Name: quotes; Type: TABLE; Schema: public; Owner: quoteuser +-- + +CREATE TABLE public.quotes ( + id integer NOT NULL, + quote_number character varying(50) NOT NULL, + customer_id integer, + quote_date date NOT NULL, + tax_exempt boolean DEFAULT false, + tax_rate numeric(5,2) DEFAULT 8.25, + subtotal numeric(10,2) DEFAULT 0, + tax_amount numeric(10,2) DEFAULT 0, + total numeric(10,2) DEFAULT 0, + has_tbd boolean DEFAULT false, + tbd_note text, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE public.quotes OWNER TO quoteuser; + +-- +-- Name: quotes_id_seq; Type: SEQUENCE; Schema: public; Owner: quoteuser +-- + +CREATE SEQUENCE public.quotes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.quotes_id_seq OWNER TO quoteuser; + +-- +-- Name: quotes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quoteuser +-- + +ALTER SEQUENCE public.quotes_id_seq OWNED BY public.quotes.id; + + +-- +-- Name: customers id; Type: DEFAULT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.customers ALTER COLUMN id SET DEFAULT nextval('public.customers_id_seq'::regclass); + + +-- +-- Name: quote_items id; Type: DEFAULT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quote_items ALTER COLUMN id SET DEFAULT nextval('public.quote_items_id_seq'::regclass); + + +-- +-- Name: quotes id; Type: DEFAULT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quotes ALTER COLUMN id SET DEFAULT nextval('public.quotes_id_seq'::regclass); + + +-- +-- Data for Name: customers; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +COPY public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) FROM stdin; +1 Braselton Development 5337 Yorktown Blvd. Suite 10-D Corpus Christi TX 78414 3617790060 2026-01-22 01:09:30.914655 2026-01-22 01:09:30.914655 +2 Karen Menn 5134 Graford Place Corpus Christi TX 78413 3619933550 2026-01-22 01:19:49.357044 2026-01-22 01:49:16.051712 +3 Hearing Aid Company of Texas 6468 Holly Road Corpus Christi TX 78412 3618143487 2026-01-22 03:33:56.090479 2026-01-22 03:33:56.090479 +4 South Shore Christian Church 4710 S. Alameda Corpus Christi TX 78412 3619926391 2026-01-22 03:40:33.012646 2026-01-22 03:40:33.012646 +5 JE Construction Services, LLC 7505 Up River Road Corpus Christi TX 78409 3612892901 2026-01-22 03:41:08.716604 2026-01-22 03:41:08.716604 +6 John T. Thompson, DDS 4101 US-77 Corpus Christi TX 78410 3612423151 2026-01-30 20:50:22.987565 2026-01-30 21:06:23.354743 +\. + + +-- +-- Data for Name: quote_items; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +COPY public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) FROM stdin; +26 5 1

HPE ProLiant MicroServer Gen11 Ultra Micro Tower Server - 1 x Intel Xeon E-2414, 32 GB DDR5 RAM

1900 1900.00 f 0 2026-01-22 18:02:30.878526 +27 5 2

Western Digital 24TB WD Red Pro NAS Internal Hard Drive HDD

850 1700.00 f 1 2026-01-22 18:02:30.878526 +28 5 1 2250 2250.00 f 2 2026-01-22 18:02:30.878526 +44 1 1 1079 1079.00 f 0 2026-01-22 18:51:00.998206 +45 1 1

DisplayPort to HDMI cable 10ft

20 20.00 f 1 2026-01-22 18:51:00.998206 +46 1 3

Setup and configure Dell OptiPlex 7010 off-site\nInstall all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network\nTransferred data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. \nSetup printing and scanning as required. Install customer requested software.Test all hardware for proper Operation

125 375.00 f 2 2026-01-22 18:51:00.998206 +47 2 1

Lenovo Yoga 7 82YN 16" i5-1335U 1.3GHz 16GB RAM 512GB SSD

500 500.00 f 0 2026-01-22 18:54:57.288474 +48 2 2

Setup and configure Lenovo yoga 7 82YN 16" i5-1335U 1.3GHz 16GB RAM 512GB SSD off-site. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Transferred data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

125 250.00 f 1 2026-01-22 18:54:57.288474 +49 4 1 2080 2080.00 f 0 2026-01-22 20:00:28.631846 +50 4 2

Setup and configure Lenovo Yoga as needed. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Transfer data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

125 250.00 f 1 2026-01-22 20:00:28.631846 +59 3 3 1949 5847.00 f 0 2026-01-26 18:41:05.501558 +60 3

Setup and configure Lenovo Laptops as needed. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

125 TBD t 1 2026-01-26 18:41:05.501558 +74 7 1

Dell Optiplex 7010 (or similar) Tower configured with Intel Core i5-13500 processor, 16GB RAM, 512GB solid state drive and Windows 11 Professional. Refurbished with a one year warranty.\t

725.00 725.00 f 0 2026-01-30 21:04:38.661279 +75 7 3

Delivery and installation of Dell Tower PC. Setup on local network and install requested software. Install and configure local and network printers. Transfer requested data from existing PC and verify proper operation of all hardware.


Microsoft Office (if required) is not included in this quote.

125 375.00 f 1 2026-01-30 21:04:38.661279 +76 6 1 1325.00 1325.00 f 0 2026-01-30 21:07:26.820637 +77 6 3

Delivery and installation of new Dell Tower PC. Setup on local network and install requested software. Install and configure local and network printers. Transfer requested data from existing PC and verify proper operation of all hardware.


Microsoft Office (if required) is not included in this quote.

125.00 375.00 f 1 2026-01-30 21:07:26.820637 +\. + + +-- +-- Data for Name: quotes; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +COPY public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) FROM stdin; +5 2026-01-0005 5 2026-01-22 f 8.25 5850.00 482.63 6332.63 f 2026-01-22 16:28:42.374654 2026-01-22 18:02:30.878526 +1 2026-01-0001 2 2026-01-22 f 8.25 1474.00 121.61 1595.61 f 2026-01-22 01:34:06.558046 2026-01-22 18:51:00.998206 +2 2026-01-0002 3 2026-01-22 f 8.25 750.00 61.88 811.88 f 2026-01-22 03:35:15.021729 2026-01-22 18:54:57.288474 +4 2026-01-0004 4 2026-01-22 f 8.25 2330.00 192.23 2522.23 f 2026-01-22 03:45:56.686598 2026-01-22 20:00:28.631846 +3 2026-01-0003 1 2026-01-26 f 8.25 5847.00 482.38 6329.38 t Total excludes labor charges which will be determined based on actual time required. 2026-01-22 03:36:47.795674 2026-01-26 18:41:05.501558 +7 2026-01-0007 6 2026-01-30 f 8.25 1100.00 90.75 1190.75 f 2026-01-30 21:01:43.538202 2026-01-30 21:04:38.661279 +6 2026-01-0006 6 2026-01-30 f 8.25 1700.00 140.25 1840.25 f 2026-01-30 20:58:23.014874 2026-01-30 21:07:26.820637 +\. + + +-- +-- Name: customers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.customers_id_seq', 6, true); + + +-- +-- Name: quote_items_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.quote_items_id_seq', 77, true); + + +-- +-- Name: quotes_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.quotes_id_seq', 7, true); + + +-- +-- Name: customers customers_pkey; Type: CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.customers + ADD CONSTRAINT customers_pkey PRIMARY KEY (id); + + +-- +-- Name: quote_items quote_items_pkey; Type: CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quote_items + ADD CONSTRAINT quote_items_pkey PRIMARY KEY (id); + + +-- +-- Name: quotes quotes_pkey; Type: CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quotes + ADD CONSTRAINT quotes_pkey PRIMARY KEY (id); + + +-- +-- Name: quotes quotes_quote_number_key; Type: CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quotes + ADD CONSTRAINT quotes_quote_number_key UNIQUE (quote_number); + + +-- +-- Name: idx_quote_items_quote_id; Type: INDEX; Schema: public; Owner: quoteuser +-- + +CREATE INDEX idx_quote_items_quote_id ON public.quote_items USING btree (quote_id); + + +-- +-- Name: idx_quotes_customer_id; Type: INDEX; Schema: public; Owner: quoteuser +-- + +CREATE INDEX idx_quotes_customer_id ON public.quotes USING btree (customer_id); + + +-- +-- Name: idx_quotes_quote_number; Type: INDEX; Schema: public; Owner: quoteuser +-- + +CREATE INDEX idx_quotes_quote_number ON public.quotes USING btree (quote_number); + + +-- +-- Name: quote_items quote_items_quote_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quote_items + ADD CONSTRAINT quote_items_quote_id_fkey FOREIGN KEY (quote_id) REFERENCES public.quotes(id) ON DELETE CASCADE; + + +-- +-- Name: quotes quotes_customer_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: quoteuser +-- + +ALTER TABLE ONLY public.quotes + ADD CONSTRAINT quotes_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES public.customers(id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict bxGU7dQ4DrNrHU2OuyEH16NHE6ZA8yFm2MADa6p2XI8qbowdWdtlaDeKSSp2NYx + diff --git a/prod_data_only.sql b/prod_data_only.sql new file mode 100644 index 0000000..e5b916c --- /dev/null +++ b/prod_data_only.sql @@ -0,0 +1,102 @@ +-- +-- PostgreSQL database dump +-- + +\restrict KCbrUeHdJ7srnFlBFWbQWdZ6A6bdMlTKPXbmEoc5qE3gaNBouFxTyfvdD9oETV4 + +-- Dumped from database version 15.15 +-- Dumped by pg_dump version 15.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Data for Name: customers; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (1, 'Braselton Development', '5337 Yorktown Blvd. Suite 10-D', 'Corpus Christi', 'TX', '78414', '3617790060', '2026-01-22 01:09:30.914655', '2026-01-22 01:09:30.914655'); +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (2, 'Karen Menn', '5134 Graford Place', 'Corpus Christi', 'TX', '78413', '3619933550', '2026-01-22 01:19:49.357044', '2026-01-22 01:49:16.051712'); +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (3, 'Hearing Aid Company of Texas', '6468 Holly Road', 'Corpus Christi', 'TX', '78412', '3618143487', '2026-01-22 03:33:56.090479', '2026-01-22 03:33:56.090479'); +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (4, 'South Shore Christian Church', '4710 S. Alameda', 'Corpus Christi', 'TX', '78412', '3619926391', '2026-01-22 03:40:33.012646', '2026-01-22 03:40:33.012646'); +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (5, 'JE Construction Services, LLC', '7505 Up River Road', 'Corpus Christi', 'TX', '78409', '3612892901', '2026-01-22 03:41:08.716604', '2026-01-22 03:41:08.716604'); +INSERT INTO public.customers (id, name, street, city, state, zip_code, account_number, created_at, updated_at) VALUES (6, 'John T. Thompson, DDS', '4101 US-77', 'Corpus Christi', 'TX', '78410', '3612423151', '2026-01-30 20:50:22.987565', '2026-01-30 21:06:23.354743'); + + +-- +-- Data for Name: quotes; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (5, '2026-01-0005', 5, '2026-01-22', false, 8.25, 5850.00, 482.63, 6332.63, false, '', '2026-01-22 16:28:42.374654', '2026-01-22 18:02:30.878526'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (1, '2026-01-0001', 2, '2026-01-22', false, 8.25, 1474.00, 121.61, 1595.61, false, '', '2026-01-22 01:34:06.558046', '2026-01-22 18:51:00.998206'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (2, '2026-01-0002', 3, '2026-01-22', false, 8.25, 750.00, 61.88, 811.88, false, '', '2026-01-22 03:35:15.021729', '2026-01-22 18:54:57.288474'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (4, '2026-01-0004', 4, '2026-01-22', false, 8.25, 2330.00, 192.23, 2522.23, false, '', '2026-01-22 03:45:56.686598', '2026-01-22 20:00:28.631846'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (3, '2026-01-0003', 1, '2026-01-26', false, 8.25, 5847.00, 482.38, 6329.38, true, 'Total excludes labor charges which will be determined based on actual time required.', '2026-01-22 03:36:47.795674', '2026-01-26 18:41:05.501558'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (7, '2026-01-0007', 6, '2026-01-30', false, 8.25, 1100.00, 90.75, 1190.75, false, '', '2026-01-30 21:01:43.538202', '2026-01-30 21:04:38.661279'); +INSERT INTO public.quotes (id, quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, tbd_note, created_at, updated_at) VALUES (6, '2026-01-0006', 6, '2026-01-30', false, 8.25, 1700.00, 140.25, 1840.25, false, '', '2026-01-30 20:58:23.014874', '2026-01-30 21:07:26.820637'); + + +-- +-- Data for Name: quote_items; Type: TABLE DATA; Schema: public; Owner: quoteuser +-- + +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (26, 5, '1', '

HPE ProLiant MicroServer Gen11 Ultra Micro Tower Server - 1 x Intel Xeon E-2414, 32 GB DDR5 RAM

', '1900', '1900.00', false, 0, '2026-01-22 18:02:30.878526'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (27, 5, '2', '

Western Digital 24TB WD Red Pro NAS Internal Hard Drive HDD

', '850', '1700.00', false, 1, '2026-01-22 18:02:30.878526'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (28, 5, '1', '', '2250', '2250.00', false, 2, '2026-01-22 18:02:30.878526'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (44, 1, '1', '', '1079', '1079.00', false, 0, '2026-01-22 18:51:00.998206'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (45, 1, '1', '

DisplayPort to HDMI cable 10ft

', '20', '20.00', false, 1, '2026-01-22 18:51:00.998206'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (46, 1, '3', '

Setup and configure Dell OptiPlex 7010 off-site +Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network +Transferred data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. +Setup printing and scanning as required. Install customer requested software.Test all hardware for proper Operation

', '125', '375.00', false, 2, '2026-01-22 18:51:00.998206'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (47, 2, '1', '

Lenovo Yoga 7 82YN 16" i5-1335U 1.3GHz 16GB RAM 512GB SSD

', '500', '500.00', false, 0, '2026-01-22 18:54:57.288474'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (48, 2, '2', '

Setup and configure Lenovo yoga 7 82YN 16" i5-1335U 1.3GHz 16GB RAM 512GB SSD off-site. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Transferred data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

', '125', '250.00', false, 1, '2026-01-22 18:54:57.288474'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (49, 4, '1', '', '2080', '2080.00', false, 0, '2026-01-22 20:00:28.631846'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (50, 4, '2', '

Setup and configure Lenovo Yoga as needed. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Transfer data from old computer including desktop items, documents, downloads, pictures, videos, browser bookmarks and favorites. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

', '125', '250.00', false, 1, '2026-01-22 20:00:28.631846'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (59, 3, '3', '', '1949', '5847.00', false, 0, '2026-01-26 18:41:05.501558'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (60, 3, '', '

Setup and configure Lenovo Laptops as needed. Install all Lenovo drivers and latest Windows updates. Deliver to site and setup on local network. Setup printing and scanning as required. Install customer requested software. Test all hardware for proper operation.

', '125', 'TBD', true, 1, '2026-01-26 18:41:05.501558'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (74, 7, '1', '

Dell Optiplex 7010 (or similar) Tower configured with Intel Core i5-13500 processor, 16GB RAM, 512GB solid state drive and Windows 11 Professional. Refurbished with a one year warranty.

', '725.00', '725.00', false, 0, '2026-01-30 21:04:38.661279'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (75, 7, '3', '

Delivery and installation of Dell Tower PC. Setup on local network and install requested software. Install and configure local and network printers. Transfer requested data from existing PC and verify proper operation of all hardware.


Microsoft Office (if required) is not included in this quote.

', '125', '375.00', false, 1, '2026-01-30 21:04:38.661279'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (76, 6, '1', '', '1325.00', '1325.00', false, 0, '2026-01-30 21:07:26.820637'); +INSERT INTO public.quote_items (id, quote_id, quantity, description, rate, amount, is_tbd, item_order, created_at) VALUES (77, 6, '3', '

Delivery and installation of new Dell Tower PC. Setup on local network and install requested software. Install and configure local and network printers. Transfer requested data from existing PC and verify proper operation of all hardware.


Microsoft Office (if required) is not included in this quote.

', '125.00', '375.00', false, 1, '2026-01-30 21:07:26.820637'); + + +-- +-- Name: customers_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.customers_id_seq', 6, true); + + +-- +-- Name: quote_items_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.quote_items_id_seq', 77, true); + + +-- +-- Name: quotes_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quoteuser +-- + +SELECT pg_catalog.setval('public.quotes_id_seq', 7, true); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict KCbrUeHdJ7srnFlBFWbQWdZ6A6bdMlTKPXbmEoc5qE3gaNBouFxTyfvdD9oETV4 + diff --git a/server.js b/server.js index 114feb9..41e9104 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,39 @@ const multer = require('multer'); const app = express(); const PORT = process.env.PORT || 3000; +// Global browser instance +let browser = null; + +// Initialize browser on startup +async function initBrowser() { + if (!browser) { + console.log('[BROWSER] Launching persistent browser...'); + browser = await puppeteer.launch({ + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-software-rasterizer', + '--no-zygote', + '--single-process' + ], + protocolTimeout: 180000, + timeout: 180000 + }); + console.log('[BROWSER] Browser launched and ready'); + + // Restart browser if it crashes + browser.on('disconnected', () => { + console.log('[BROWSER] Browser disconnected, restarting...'); + browser = null; + initBrowser(); + }); + } + return browser; +} + // Database connection const pool = new Pool({ user: process.env.DB_USER || 'postgres', @@ -39,7 +72,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit + limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); @@ -226,7 +259,6 @@ app.post('/api/quotes', async (req, res) => { const quote_number = await getNextQuoteNumber(); - // Calculate totals let subtotal = 0; let has_tbd = false; @@ -245,7 +277,6 @@ app.post('/api/quotes', async (req, res) => { const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; - // Insert quote const quoteResult = await client.query( `INSERT INTO quotes (quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, @@ -254,7 +285,6 @@ app.post('/api/quotes', async (req, res) => { const quoteId = quoteResult.rows[0].id; - // Insert items for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO quote_items (quote_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', @@ -281,7 +311,6 @@ app.put('/api/quotes/:id', async (req, res) => { try { await client.query('BEGIN'); - // Calculate totals let subtotal = 0; let has_tbd = false; @@ -300,7 +329,6 @@ app.put('/api/quotes/:id', async (req, res) => { const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; - // Update quote await client.query( `UPDATE quotes SET customer_id = $1, quote_date = $2, tax_exempt = $3, tax_rate = $4, subtotal = $5, tax_amount = $6, total = $7, has_tbd = $8, updated_at = CURRENT_TIMESTAMP @@ -308,7 +336,6 @@ app.put('/api/quotes/:id', async (req, res) => { [customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, id] ); - // Delete old items and insert new ones await client.query('DELETE FROM quote_items WHERE quote_id = $1', [id]); for (let i = 0; i < items.length; i++) { @@ -401,7 +428,6 @@ app.post('/api/invoices', async (req, res) => { const invoice_number = await getNextInvoiceNumber(); - // Calculate totals - invoices should NOT have TBD items let subtotal = 0; for (const item of items) { @@ -415,7 +441,6 @@ app.post('/api/invoices', async (req, res) => { const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; - // Insert invoice const invoiceResult = await client.query( `INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, @@ -424,7 +449,6 @@ app.post('/api/invoices', async (req, res) => { const invoiceId = invoiceResult.rows[0].id; - // Insert items for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', @@ -450,7 +474,6 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { try { await client.query('BEGIN'); - // Get quote details const quoteResult = await pool.query(` SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM quotes q @@ -465,13 +488,11 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { const quote = quoteResult.rows[0]; - // Get quote items const itemsResult = await pool.query( 'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order', [id] ); - // Check for TBD items const hasTBD = itemsResult.rows.some(item => item.rate.toUpperCase() === 'TBD' || item.amount.toUpperCase() === 'TBD' ); @@ -481,7 +502,6 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { return res.status(400).json({ error: 'Cannot convert quote with TBD items to invoice. Please update all TBD items first.' }); } - // Create invoice const invoice_number = await getNextInvoiceNumber(); const invoiceDate = new Date().toISOString().split('T')[0]; @@ -493,7 +513,6 @@ app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { const invoiceId = invoiceResult.rows[0].id; - // Copy items to invoice for (let i = 0; i < itemsResult.rows.length; i++) { const item = itemsResult.rows[i]; await client.query( @@ -521,7 +540,6 @@ app.put('/api/invoices/:id', async (req, res) => { try { await client.query('BEGIN'); - // Calculate totals let subtotal = 0; for (const item of items) { @@ -535,7 +553,6 @@ app.put('/api/invoices/:id', async (req, res) => { const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; - // Update invoice await client.query( `UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5, tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, updated_at = CURRENT_TIMESTAMP @@ -543,7 +560,6 @@ app.put('/api/invoices/:id', async (req, res) => { [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id] ); - // Delete old items and insert new ones await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); for (let i = 0; i < items.length; i++) { @@ -582,14 +598,13 @@ app.delete('/api/invoices/:id', async (req, res) => { } }); -// PDF Generation for Quotes +// PDF Generation using templates and persistent browser app.get('/api/quotes/:id/pdf', async (req, res) => { const { id } = req.params; console.log(`[PDF] Starting quote PDF generation for ID: ${id}`); try { - console.log('[PDF] Fetching quote from database...'); const quoteResult = await pool.query(` SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM quotes q @@ -598,92 +613,118 @@ app.get('/api/quotes/:id/pdf', async (req, res) => { `, [id]); if (quoteResult.rows.length === 0) { - console.log('[PDF] Quote not found'); return res.status(404).json({ error: 'Quote not found' }); } const quote = quoteResult.rows[0]; - console.log(`[PDF] Quote loaded: ${quote.quote_number}`); - - console.log('[PDF] Fetching quote items...'); const itemsResult = await pool.query( 'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order', [id] ); - console.log(`[PDF] Items loaded: ${itemsResult.rows.length} items`); - console.log('[PDF] Generating HTML...'); - const html = await generateQuotePDFHTML(quote, itemsResult.rows); - console.log(`[PDF] HTML generated, length: ${html.length} chars`); + // Load template and replace placeholders + const templatePath = path.join(__dirname, 'templates', 'quote-template.html'); + let html = await fs.readFile(templatePath, 'utf-8'); - console.log('[PDF] Launching Puppeteer...'); - const browser = await puppeteer.launch({ - headless: 'new', - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu', - '--disable-software-rasterizer', - '--no-zygote', - '--single-process' // Wichtig für Docker! - ], - protocolTimeout: 180000, // 3 Minuten - timeout: 180000 - }); - console.log('[PDF] Browser launched successfully'); + // Get logo + let logoHTML = ''; + try { + const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); + const logoData = await fs.readFile(logoPath); + const logoBase64 = logoData.toString('base64'); + logoHTML = ``; + } catch (err) { + // No logo + } - console.log('[PDF] Creating new page...'); - const page = await browser.newPage(); - console.log('[PDF] Page created'); + // Generate items HTML + let itemsHTML = itemsResult.rows.map(item => ` + + ${item.quantity} + ${item.description} + ${item.rate} + ${item.amount} + + `).join(''); - console.log('[PDF] Setting content...'); - await page.setContent(html, { - waitUntil: 'networkidle0', - timeout: 60000 - }); - console.log('[PDF] Content set'); + // Add totals + itemsHTML += ` + + Subtotal: + $${parseFloat(quote.subtotal).toFixed(2)} + `; + + if (!quote.tax_exempt) { + itemsHTML += ` + + Tax (${quote.tax_rate}%): + $${parseFloat(quote.tax_amount).toFixed(2)} + `; + } + + itemsHTML += ` + + TOTAL: + $${parseFloat(quote.total).toFixed(2)} + + + Thank you for your business! + `; + + let tbdNote = ''; + if (quote.has_tbd) { + tbdNote = '

* Note: This quote contains items marked as "TBD" (To Be Determined). The final total may vary once all details are finalized.

'; + } + + // Replace placeholders + html = html + .replace('{{LOGO_HTML}}', logoHTML) + .replace('{{CUSTOMER_NAME}}', quote.customer_name || '') + .replace('{{CUSTOMER_STREET}}', quote.street || '') + .replace('{{CUSTOMER_CITY}}', quote.city || '') + .replace('{{CUSTOMER_STATE}}', quote.state || '') + .replace('{{CUSTOMER_ZIP}}', quote.zip_code || '') + .replace('{{QUOTE_NUMBER}}', quote.quote_number) + .replace('{{ACCOUNT_NUMBER}}', quote.account_number || '') + .replace('{{QUOTE_DATE}}', formatDate(quote.quote_date)) + .replace('{{ITEMS}}', itemsHTML) + .replace('{{TBD_NOTE}}', tbdNote); + + // Use persistent browser + const browserInstance = await initBrowser(); + const page = await browserInstance.newPage(); + + await page.setContent(html, { waitUntil: 'networkidle0', timeout: 60000 }); - console.log('[PDF] Generating PDF...'); const pdf = await page.pdf({ format: 'Letter', printBackground: true, - margin: { - top: '0.5in', - right: '0.5in', - bottom: '0.5in', - left: '0.5in' - }, + margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, timeout: 60000 }); - console.log(`[PDF] PDF generated, size: ${pdf.length} bytes`); - console.log('[PDF] Closing browser...'); - await browser.close(); - console.log('[PDF] Browser closed'); - + await page.close(); // Close page, not browser + res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length, 'Content-Disposition': `attachment; filename="Quote-${quote.quote_number}.pdf"` }); res.end(pdf, 'binary'); - console.log('[PDF] PDF sent to client successfully'); + console.log('[PDF] Quote PDF sent successfully'); + } catch (error) { console.error('[PDF] ERROR:', error); - console.error('[PDF] Stack:', error.stack); res.status(500).json({ error: 'Error generating PDF', details: error.message }); } }); -// PDF Generation for Invoices app.get('/api/invoices/:id/pdf', async (req, res) => { const { id } = req.params; console.log(`[INVOICE-PDF] Starting invoice PDF generation for ID: ${id}`); try { - console.log('[INVOICE-PDF] Fetching invoice from database...'); const invoiceResult = await pool.query(` SELECT i.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM invoices i @@ -692,727 +733,128 @@ app.get('/api/invoices/:id/pdf', async (req, res) => { `, [id]); if (invoiceResult.rows.length === 0) { - console.log('[INVOICE-PDF] Invoice not found'); return res.status(404).json({ error: 'Invoice not found' }); } const invoice = invoiceResult.rows[0]; - console.log(`[INVOICE-PDF] Invoice loaded: ${invoice.invoice_number}`); - - console.log('[INVOICE-PDF] Fetching invoice items...'); const itemsResult = await pool.query( 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [id] ); - console.log(`[INVOICE-PDF] Items loaded: ${itemsResult.rows.length} items`); - console.log('[INVOICE-PDF] Generating HTML...'); - const html = await generateInvoicePDFHTML(invoice, itemsResult.rows); - console.log(`[INVOICE-PDF] HTML generated, length: ${html.length} chars`); + // Load template + const templatePath = path.join(__dirname, 'templates', 'invoice-template.html'); + let html = await fs.readFile(templatePath, 'utf-8'); - console.log('[INVOICE-PDF] Launching Puppeteer...'); - const browser = await puppeteer.launch({ - headless: 'new', - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu', - '--disable-software-rasterizer', - '--no-zygote', - '--single-process' - ], - protocolTimeout: 180000, - timeout: 180000 - }); - console.log('[INVOICE-PDF] Browser launched successfully'); + // Get logo + let logoHTML = ''; + try { + const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); + const logoData = await fs.readFile(logoPath); + const logoBase64 = logoData.toString('base64'); + logoHTML = ``; + } catch (err) { + // No logo + } - console.log('[INVOICE-PDF] Creating new page...'); - const page = await browser.newPage(); - console.log('[INVOICE-PDF] Page created'); + // Generate items HTML + let itemsHTML = itemsResult.rows.map(item => ` + + ${item.quantity} + ${item.description} + ${item.rate} + ${item.amount} + + `).join(''); - console.log('[INVOICE-PDF] Setting content...'); - await page.setContent(html, { - waitUntil: 'networkidle0', - timeout: 60000 - }); - console.log('[INVOICE-PDF] Content set'); + // Add totals + itemsHTML += ` + + Subtotal: + $${parseFloat(invoice.subtotal).toFixed(2)} + `; + + if (!invoice.tax_exempt) { + itemsHTML += ` + + Tax (${invoice.tax_rate}%): + $${parseFloat(invoice.tax_amount).toFixed(2)} + `; + } + + itemsHTML += ` + + TOTAL: + $${parseFloat(invoice.total).toFixed(2)} + + + Thank you for your business! + `; + + // Authorization field + const authHTML = invoice.auth_code ? + `

Authorization: ${invoice.auth_code}

` : ''; + + // Replace placeholders + html = html + .replace('{{LOGO_HTML}}', logoHTML) + .replace('{{CUSTOMER_NAME}}', invoice.customer_name || '') + .replace('{{CUSTOMER_STREET}}', invoice.street || '') + .replace('{{CUSTOMER_CITY}}', invoice.city || '') + .replace('{{CUSTOMER_STATE}}', invoice.state || '') + .replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '') + .replace('{{INVOICE_NUMBER}}', invoice.invoice_number) + .replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '') + .replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date)) + .replace('{{TERMS}}', invoice.terms) + .replace('{{AUTHORIZATION}}', authHTML) + .replace('{{ITEMS}}', itemsHTML); + + // Use persistent browser + const browserInstance = await initBrowser(); + const page = await browserInstance.newPage(); + + await page.setContent(html, { waitUntil: 'networkidle0', timeout: 60000 }); - console.log('[INVOICE-PDF] Generating PDF...'); const pdf = await page.pdf({ format: 'Letter', printBackground: true, - margin: { - top: '0.5in', - right: '0.5in', - bottom: '0.5in', - left: '0.5in' - }, + margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, timeout: 60000 }); - console.log(`[INVOICE-PDF] PDF generated, size: ${pdf.length} bytes`); - console.log('[INVOICE-PDF] Closing browser...'); - await browser.close(); - console.log('[INVOICE-PDF] Browser closed'); - + await page.close(); // Close page, not browser + res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length, 'Content-Disposition': `attachment; filename="Invoice-${invoice.invoice_number}.pdf"` }); res.end(pdf, 'binary'); - console.log('[INVOICE-PDF] PDF sent to client successfully'); + console.log('[INVOICE-PDF] Invoice PDF sent successfully'); } catch (error) { console.error('[INVOICE-PDF] ERROR:', error); - console.error('[INVOICE-PDF] Stack:', error.stack); res.status(500).json({ error: 'Error generating PDF', details: error.message }); } }); -async function generateQuotePDFHTML(quote, items) { - // Check if logo exists - const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); - let logoHTML = ''; - try { - const logoData = await fs.readFile(logoPath); - const logoBase64 = logoData.toString('base64'); - logoHTML = ``; - } catch (error) { - // No logo, continue without it - } +// Start server and browser +async function startServer() { + await initBrowser(); - // Generate items HTML - let itemsHTML = items.map(item => ` - - ${item.quantity} - ${item.description} - ${item.rate} - ${item.amount} - - `).join(''); - - // Add totals - itemsHTML += ` - - Subtotal: - $${parseFloat(quote.subtotal).toFixed(2)} - `; - - if (!quote.tax_exempt) { - itemsHTML += ` - - Tax (${quote.tax_rate}%): - $${parseFloat(quote.tax_amount).toFixed(2)} - `; - } - - itemsHTML += ` - - TOTAL: - $${parseFloat(quote.total).toFixed(2)} - - - Thank you for your business! - `; - - // TBD note if applicable - let tbdNote = ''; - if (quote.has_tbd) { - tbdNote = '

* Note: This quote contains items marked as "TBD" (To Be Determined). The final total may vary once all details are finalized.

'; - } - - return ` - - - - - - -
-
-
- ${logoHTML} -
-

Bay Area Affiliates, Inc.

-

1001 Blucher Street
- Corpus Christi, Texas 78401

-
-
-
-
- Providing IT Services and Support in South Texas Since 1996 -
-
- Phone:
- (361) 765-8400
- (361) 765-8401
- (361) 232-6578
- Email:
- support@bayarea-cc.com -
-
-
- -
-
-
Quote For:
-
- ${quote.customer_name}
- ${quote.street}
- ${quote.city}, ${quote.state} ${quote.zip_code} -
-
- - - - - - - - - - - - - - - -
QUOTE #ACCOUNT NO.DATE
${quote.quote_number}${quote.account_number || ''}${formatDate(quote.quote_date)}
-
- - - - - - - - - - - - ${itemsHTML} - -
QTYDESCRIPTIONRATEAMOUNT
- ${tbdNote} -
- -`; + app.listen(PORT, () => { + console.log(`Quote System running on port ${PORT}`); + }); } -async function generateInvoicePDFHTML(invoice, items) { - // Check if logo exists - const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); - let logoHTML = ''; - try { - const logoData = await fs.readFile(logoPath); - const logoBase64 = logoData.toString('base64'); - logoHTML = ``; - } catch (error) { - // No logo, continue without it - } - - // Generate items HTML - let itemsHTML = items.map(item => ` - - ${item.quantity} - ${item.description} - ${item.rate} - ${item.amount} - - `).join(''); - - // Add totals - itemsHTML += ` - - Subtotal: - $${parseFloat(invoice.subtotal).toFixed(2)} - `; - - if (!invoice.tax_exempt) { - itemsHTML += ` - - Tax (${invoice.tax_rate}%): - $${parseFloat(invoice.tax_amount).toFixed(2)} - `; - } - - itemsHTML += ` - - TOTAL: - $${parseFloat(invoice.total).toFixed(2)} - - - Thank you for your business! - `; - - return ` - - - - - - -
-
-
- ${logoHTML} -
-

Bay Area Affiliates, Inc.

-

1001 Blucher Street
- Corpus Christi, Texas 78401

-
-
-
-
- Providing IT Services and Support in South Texas Since 1996 -
-
- Phone:
- (361) 765-8400
- (361) 765-8401
- (361) 232-6578
- Email:
- accounting@bayarea-cc.com -
-
-
- -
-
-
Bill To:
-
- ${invoice.customer_name}
- ${invoice.street}
- ${invoice.city}, ${invoice.state} ${invoice.zip_code} -
-
- - - - - - - - - - - - - - - - - -
INVOICE #ACCOUNT NO.DATETERMS
${invoice.invoice_number}${invoice.account_number || ''}${formatDate(invoice.invoice_date)}${invoice.terms}
-
- ${invoice.auth_code ? `

Authorization: ${invoice.auth_code}

` : ''} - - - - - - - - - - - - ${itemsHTML} - -
QTYDESCRIPTIONRATEAMOUNT
-
- -`; -} - -// Start server -app.listen(PORT, () => { - console.log(`Quote System running on port ${PORT}`); -}); +startServer(); // Graceful shutdown process.on('SIGTERM', async () => { + if (browser) { + await browser.close(); + } await pool.end(); process.exit(0); -}); +}); \ No newline at end of file diff --git a/templates/invoice-template.html b/templates/invoice-template.html new file mode 100644 index 0000000..beab5cc --- /dev/null +++ b/templates/invoice-template.html @@ -0,0 +1,270 @@ + + + + + + + +
+
+
+ {{LOGO_HTML}} +
+

Bay Area Affiliates, Inc.

+

1001 Blucher Street
+ Corpus Christi, Texas 78401

+
+
+
+
+ Providing IT Services and Support in South Texas Since 1996 +
+
+ Phone:
+ (361) 765-8400
+ (361) 765-8401
+ (361) 232-6578
+ Email:
+ accounting@bayarea-cc.com +
+
+
+ +
+
+
Bill To:
+
+ {{CUSTOMER_NAME}}
+ {{CUSTOMER_STREET}}
+ {{CUSTOMER_CITY}}, {{CUSTOMER_STATE}} {{CUSTOMER_ZIP}} +
+
+ + + + + + + + + + + + + + + + + +
INVOICE #ACCOUNT NO.DATETERMS
{{INVOICE_NUMBER}}{{ACCOUNT_NUMBER}}{{INVOICE_DATE}}{{TERMS}}
+
+ + {{AUTHORIZATION}} + + + + + + + + + + + + {{ITEMS}} + +
QTYDESCRIPTIONRATEAMOUNT
+
+ + \ No newline at end of file diff --git a/templates/quote-template.html b/templates/quote-template.html new file mode 100644 index 0000000..b9f3e66 --- /dev/null +++ b/templates/quote-template.html @@ -0,0 +1,267 @@ + + + + + + + +
+
+
+ {{LOGO_HTML}} +
+

Bay Area Affiliates, Inc.

+

1001 Blucher Street
+ Corpus Christi, Texas 78401

+
+
+
+
+ Providing IT Services and Support in South Texas Since 1996 +
+
+ Phone:
+ (361) 765-8400
+ (361) 765-8401
+ (361) 232-6578
+ Email:
+ support@bayarea-cc.com +
+
+
+ +
+
+
Quote For:
+
+ {{CUSTOMER_NAME}}
+ {{CUSTOMER_STREET}}
+ {{CUSTOMER_CITY}}, {{CUSTOMER_STATE}} {{CUSTOMER_ZIP}} +
+
+ + + + + + + + + + + + + + + +
QUOTE #ACCOUNT NO.DATE
{{QUOTE_NUMBER}}{{ACCOUNT_NUMBER}}{{QUOTE_DATE}}
+
+ + + + + + + + + + + + {{ITEMS}} + +
QTYDESCRIPTIONRATEAMOUNT
+ {{TBD_NOTE}} +
+ + \ No newline at end of file