customer module + features
This commit is contained in:
125
server.js
125
server.js
@@ -165,23 +165,19 @@ app.get('/api/customers', async (req, res) => {
|
||||
|
||||
// POST /api/customers
|
||||
app.post('/api/customers', async (req, res) => {
|
||||
// line1 bis line4 statt street/pobox/suite
|
||||
const {
|
||||
name, line1, line2, line3, line4, city, state, zip_code,
|
||||
account_number, email, phone, phone2, taxable
|
||||
const {
|
||||
name, contact, line1, line2, line3, line4, city, state, zip_code,
|
||||
account_number, email, phone, phone2, taxable, remarks
|
||||
} = req.body;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO customers
|
||||
(name, line1, line2, line3, line4, city, state,
|
||||
zip_code, account_number, email, phone, phone2, taxable)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING *`,
|
||||
[name, line1 || null, line2 || null, line3 || null, line4 || null,
|
||||
city || null, state || null, zip_code || null,
|
||||
account_number || null, email || null, phone || null, phone2 || null,
|
||||
taxable !== undefined ? taxable : true]
|
||||
`INSERT INTO customers (name, contact, line1, line2, line3, line4, city, state, zip_code, account_number, email, phone, phone2, taxable, remarks)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`,
|
||||
[name, contact || null, line1 || null, line2 || null, line3 || null, line4 || null,
|
||||
city || null, state || null, zip_code || null, account_number || null,
|
||||
email || null, phone || null, phone2 || null,
|
||||
taxable !== undefined ? taxable : true, remarks || null]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
@@ -194,28 +190,27 @@ app.post('/api/customers', async (req, res) => {
|
||||
app.put('/api/customers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name, line1, line2, line3, line4, city, state, zip_code,
|
||||
account_number, email, phone, phone2, taxable
|
||||
name, contact, line1, line2, line3, line4, city, state, zip_code,
|
||||
account_number, email, phone, phone2, taxable, remarks
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
// Lokal updaten
|
||||
const result = await pool.query(
|
||||
`UPDATE customers
|
||||
SET name = $1, line1 = $2, line2 = $3, line3 = $4, line4 = $5,
|
||||
city = $6, state = $7, zip_code = $8, account_number = $9, email = $10,
|
||||
phone = $11, phone2 = $12, taxable = $13, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $14
|
||||
SET name = $1, contact = $2, line1 = $3, line2 = $4, line3 = $5, line4 = $6,
|
||||
city = $7, state = $8, zip_code = $9, account_number = $10, email = $11,
|
||||
phone = $12, phone2 = $13, taxable = $14, remarks = $15, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $16
|
||||
RETURNING *`,
|
||||
[name, line1 || null, line2 || null, line3 || null, line4 || null,
|
||||
city || null, state || null, zip_code || null,
|
||||
account_number || null, email || null, phone || null, phone2 || null,
|
||||
taxable !== undefined ? taxable : true, id]
|
||||
[name, contact || null, line1 || null, line2 || null, line3 || null, line4 || null,
|
||||
city || null, state || null, zip_code || null, account_number || null,
|
||||
email || null, phone || null, phone2 || null,
|
||||
taxable !== undefined ? taxable : true, remarks || null, id]
|
||||
);
|
||||
|
||||
const customer = result.rows[0];
|
||||
|
||||
// In QBO updaten falls vorhanden
|
||||
// QBO Update
|
||||
if (customer.qbo_id) {
|
||||
try {
|
||||
const oauthClient = getOAuthClient();
|
||||
@@ -224,7 +219,7 @@ app.put('/api/customers/:id', async (req, res) => {
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
// Aktuellen SyncToken holen
|
||||
// SyncToken holen
|
||||
const qboRes = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/customer/${customer.qbo_id}`,
|
||||
method: 'GET'
|
||||
@@ -233,7 +228,6 @@ app.put('/api/customers/:id', async (req, res) => {
|
||||
const syncToken = qboData.Customer?.SyncToken;
|
||||
|
||||
if (syncToken !== undefined) {
|
||||
// Sparse update
|
||||
const updatePayload = {
|
||||
Id: customer.qbo_id,
|
||||
SyncToken: syncToken,
|
||||
@@ -242,9 +236,21 @@ app.put('/api/customers/:id', async (req, res) => {
|
||||
CompanyName: name,
|
||||
PrimaryEmailAddr: email ? { Address: email } : undefined,
|
||||
PrimaryPhone: phone ? { FreeFormNumber: phone } : undefined,
|
||||
Taxable: taxable !== false
|
||||
Taxable: taxable !== false,
|
||||
Notes: remarks || undefined
|
||||
};
|
||||
|
||||
// Contact → GivenName / FamilyName
|
||||
if (contact) {
|
||||
const parts = contact.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
updatePayload.GivenName = parts[0];
|
||||
updatePayload.FamilyName = parts.slice(1).join(' ');
|
||||
} else {
|
||||
updatePayload.GivenName = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Adresse
|
||||
const addr = {};
|
||||
if (line1) addr.Line1 = line1;
|
||||
@@ -269,7 +275,6 @@ app.put('/api/customers/:id', async (req, res) => {
|
||||
}
|
||||
} catch (qboError) {
|
||||
console.error(`⚠️ QBO update failed for Customer ${customer.qbo_id}:`, qboError.message);
|
||||
// Nicht abbrechen — lokales Update war erfolgreich
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2118,12 +2123,9 @@ app.post('/api/customers/:id/export-qbo', async (req, res) => {
|
||||
try {
|
||||
const custResult = await pool.query('SELECT * FROM customers WHERE id = $1', [id]);
|
||||
if (custResult.rows.length === 0) return res.status(404).json({ error: 'Customer not found' });
|
||||
|
||||
const customer = custResult.rows[0];
|
||||
|
||||
if (customer.qbo_id) {
|
||||
return res.status(400).json({ error: `Kunde "${customer.name}" ist bereits in QBO (ID: ${customer.qbo_id}).` });
|
||||
}
|
||||
if (customer.qbo_id) return res.status(400).json({ error: 'Customer already in QBO' });
|
||||
|
||||
const oauthClient = getOAuthClient();
|
||||
const companyId = oauthClient.getToken().realmId;
|
||||
@@ -2131,19 +2133,28 @@ app.post('/api/customers/:id/export-qbo', async (req, res) => {
|
||||
? 'https://quickbooks.api.intuit.com'
|
||||
: 'https://sandbox-quickbooks.api.intuit.com';
|
||||
|
||||
// QBO Customer Objekt
|
||||
const qboCustomer = {
|
||||
DisplayName: customer.name,
|
||||
CompanyName: customer.name,
|
||||
BillAddr: {},
|
||||
PrimaryEmailAddr: customer.email ? { Address: customer.email } : undefined,
|
||||
PrimaryPhone: customer.phone ? { FreeFormNumber: customer.phone } : undefined,
|
||||
// Taxable setzt man über TaxExemptionReasonId oder SalesTermRef
|
||||
Taxable: customer.taxable !== false
|
||||
Taxable: customer.taxable !== false,
|
||||
Notes: customer.remarks || undefined
|
||||
};
|
||||
|
||||
// Adresse aufbauen
|
||||
const addr = qboCustomer.BillAddr;
|
||||
// Contact
|
||||
if (customer.contact) {
|
||||
const parts = customer.contact.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
qboCustomer.GivenName = parts[0];
|
||||
qboCustomer.FamilyName = parts.slice(1).join(' ');
|
||||
} else {
|
||||
qboCustomer.GivenName = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Address
|
||||
const addr = {};
|
||||
if (customer.line1) addr.Line1 = customer.line1;
|
||||
if (customer.line2) addr.Line2 = customer.line2;
|
||||
if (customer.line3) addr.Line3 = customer.line3;
|
||||
@@ -2151,11 +2162,7 @@ app.post('/api/customers/:id/export-qbo', async (req, res) => {
|
||||
if (customer.city) addr.City = customer.city;
|
||||
if (customer.state) addr.CountrySubDivisionCode = customer.state;
|
||||
if (customer.zip_code) addr.PostalCode = customer.zip_code;
|
||||
|
||||
// Kein leeres BillAddr senden
|
||||
if (Object.keys(addr).length === 0) delete qboCustomer.BillAddr;
|
||||
|
||||
console.log(`📤 Exportiere Kunde "${customer.name}" nach QBO...`);
|
||||
if (Object.keys(addr).length > 0) qboCustomer.BillAddr = addr;
|
||||
|
||||
const response = await makeQboApiCall({
|
||||
url: `${baseUrl}/v3/company/${companyId}/customer`,
|
||||
@@ -2165,35 +2172,21 @@ app.post('/api/customers/:id/export-qbo', async (req, res) => {
|
||||
});
|
||||
|
||||
const data = response.getJson ? response.getJson() : response.json;
|
||||
const qboId = data.Customer?.Id;
|
||||
|
||||
if (data.Customer) {
|
||||
const qboId = data.Customer.Id;
|
||||
if (!qboId) throw new Error('QBO returned no ID');
|
||||
|
||||
// qbo_id lokal speichern
|
||||
await pool.query(
|
||||
'UPDATE customers SET qbo_id = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
[qboId, id]
|
||||
);
|
||||
await pool.query('UPDATE customers SET qbo_id = $1 WHERE id = $2', [qboId, id]);
|
||||
|
||||
console.log(`✅ Kunde "${customer.name}" in QBO erstellt: ID ${qboId}`);
|
||||
res.json({ success: true, qbo_id: qboId, name: customer.name });
|
||||
} else {
|
||||
console.error('❌ QBO Customer Fehler:', JSON.stringify(data));
|
||||
|
||||
// Spezieller Fehler: Name existiert schon in QBO
|
||||
const errMsg = data.Fault?.Error?.[0]?.Message || JSON.stringify(data);
|
||||
const errDetail = data.Fault?.Error?.[0]?.Detail || '';
|
||||
|
||||
res.status(500).json({ error: `QBO Fehler: ${errMsg}. ${errDetail}` });
|
||||
}
|
||||
console.log(`✅ Customer "${customer.name}" exported to QBO (ID: ${qboId})`);
|
||||
res.json({ success: true, qbo_id: qboId, name: customer.name });
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Customer Export Error:', error);
|
||||
res.status(500).json({ error: 'Export fehlgeschlagen: ' + error.message });
|
||||
console.error('QBO Customer Export Error:', error);
|
||||
res.status(500).json({ error: 'Export failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- 2. Labor Rate aus QBO laden ---
|
||||
// Lädt den UnitPrice des "Labor" Items (ID 5) aus QBO
|
||||
app.get('/api/qbo/labor-rate', async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user