diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index e864333..1ac11cb 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -12,6 +12,7 @@ application{ dependencies{ implementation(project(":company")) + implementation(project(":contact")) implementation(project(":core")) implementation(project(":documents")) implementation(project(":legacy")) diff --git a/company/build.gradle.kts b/company/build.gradle.kts index 6b72887..7b676e3 100644 --- a/company/build.gradle.kts +++ b/company/build.gradle.kts @@ -4,5 +4,6 @@ dependencies{ implementation(project(":core")) implementation("de.srsoftware:configuration.api:1.0.2") implementation("de.srsoftware:tools.jdbc:1.3.2") + implementation("de.srsoftware:tools.util:2.0.3") } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java index f679778..a2aab39 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java @@ -38,6 +38,16 @@ public class CompanyModule implements CompanyService { return members; } + @Override + public Collection listCompaniesOf(UmbrellaUser user) throws UmbrellaException { + return companyDb.listCompaniesOf(user.id()); + } + + @Override + public boolean membership(long companyId, long userId) throws UmbrellaException { + return companyDb.getMembers(companyId).contains(userId); + } + @Override public UserService userService() { return users; diff --git a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java index ce0d940..7368814 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java @@ -6,6 +6,7 @@ import static de.srsoftware.tools.jdbc.Query.select; import static de.srsoftware.umbrella.company.Constants.TABLE_COMPANIES; import static de.srsoftware.umbrella.company.Constants.TABLE_COMPANIES_USERS; import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; import de.srsoftware.umbrella.company.api.CompanyDb; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; @@ -32,7 +33,20 @@ public class SqliteDb implements CompanyDb { rs.close(); return ids; } catch (SQLException e) { - throw new UmbrellaException("Failed to load members of company {0}",companyId); + throw databaseException("Failed to load members of company {0}",companyId); + } + } + + @Override + public Collection listCompaniesOf(long userId) throws UmbrellaException { + try { + var rs = select("*").from(TABLE_COMPANIES).leftJoin(ID,TABLE_COMPANIES_USERS,COMPANY_ID).where(USER_ID,equal(userId)).exec(db); + var companies = new HashSet(); + while (rs.next()) companies.add(Company.of(rs)); + rs.close(); + return companies; + } catch (SQLException e) { + throw databaseException("Failed to load companies for user {0}",userId); } } @@ -46,7 +60,7 @@ public class SqliteDb implements CompanyDb { if (company == null) throw new UmbrellaException("Could not load company {0}",companyId); return company; } catch (SQLException e){ - throw new UmbrellaException("Could not load company {0}",companyId); + throw databaseException("Could not load company {0}",companyId); } } } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java b/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java index 883a504..58b7386 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java @@ -8,5 +8,7 @@ import java.util.Collection; public interface CompanyDb { Collection getMembers(long companyId) throws UmbrellaException; + Collection listCompaniesOf(long id) throws UmbrellaException; + Company load(long companyId) throws UmbrellaException; } diff --git a/contact/build.gradle.kts b/contact/build.gradle.kts new file mode 100644 index 0000000..e9883f5 --- /dev/null +++ b/contact/build.gradle.kts @@ -0,0 +1,8 @@ +description = "Umbrella : Documents" + +dependencies{ + implementation(project(":core")) + implementation("de.srsoftware:configuration.api:1.0.2") + implementation("de.srsoftware:tools.jdbc:1.3.2") + implementation("de.srsoftware:tools.util:2.0.3") +} \ No newline at end of file diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/Constants.java b/contact/src/main/java/de/srsoftware/umbrella/contact/Constants.java new file mode 100644 index 0000000..9dcb66d --- /dev/null +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/Constants.java @@ -0,0 +1,10 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.contact; + +public class Constants { + private Constants(){} + + public static final String CONFIG_DATABASE = "umbrella.modules.contact.database"; + public static final String TABLE_CONTACTS_USERS = "contacts_users"; + public static final String TABLE_CONTACTS = "contacts"; +} diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java new file mode 100644 index 0000000..23133c6 --- /dev/null +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java @@ -0,0 +1,10 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.contact; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Contact; +import java.util.Collection; + +public interface ContactDb { + Collection listContactsOf(long id) throws UmbrellaException; +} diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java new file mode 100644 index 0000000..3675464 --- /dev/null +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java @@ -0,0 +1,27 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.contact; + +import static de.srsoftware.umbrella.contact.Constants.CONFIG_DATABASE; +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; + +import de.srsoftware.configuration.Configuration; +import de.srsoftware.umbrella.core.api.ContactService; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Contact; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Collection; + +public class ContactModule implements ContactService { + private final ContactDb contactDb; + + public ContactModule(Configuration config) throws UmbrellaException { + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + contactDb = new SqliteDb(connect(dbFile)); + } + + @Override + public Collection listContactsOf(UmbrellaUser user) throws UmbrellaException { + return contactDb.listContactsOf(user.id()); + } +} diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java new file mode 100644 index 0000000..1b10bea --- /dev/null +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java @@ -0,0 +1,36 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.contact; + +import static de.srsoftware.tools.jdbc.Condition.equal; +import static de.srsoftware.tools.jdbc.Query.select; +import static de.srsoftware.umbrella.contact.Constants.TABLE_CONTACTS; +import static de.srsoftware.umbrella.contact.Constants.TABLE_CONTACTS_USERS; +import static de.srsoftware.umbrella.core.Constants.*; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Contact; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; + +public class SqliteDb implements ContactDb{ + private final Connection conn; + + public SqliteDb(Connection connection) { + conn = connection; + } + + @Override + public Collection listContactsOf(long userId) throws UmbrellaException{ + try { + var rs = select("*").from(TABLE_CONTACTS).leftJoin(ID,TABLE_CONTACTS_USERS,USER_ID).where(USER_ID,equal(userId)).exec(conn); + var contacts = new HashSet(); + while (rs.next()) contacts.add(Contact.of(rs)); + rs.close(); + return contacts; + } catch (SQLException e) { + throw UmbrellaException.databaseException("Failed to load contacts og user {0}",userId); + } + } +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index 650232d..ea8eb0e 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -6,6 +6,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class Constants { + private Constants(){} public static final String ADDRESS = "address"; public static final String ATTACHMENTS = "attachments"; public static final String AUTHORIZATION = "Authorization"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java index 88d0d9b..2aee4ff 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java @@ -7,8 +7,13 @@ import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.util.Collection; public interface CompanyService { - public Collection getMembers(long companyId) throws UmbrellaException; - public UserService userService(); - Company get(long companyId) throws UmbrellaException; + + Collection getMembers(long companyId) throws UmbrellaException; + + Collection listCompaniesOf(UmbrellaUser user) throws UmbrellaException; + + boolean membership(long companyId, long userId) throws UmbrellaException; + + UserService userService(); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java new file mode 100644 index 0000000..87a6d2e --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java @@ -0,0 +1,11 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.api; + +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Contact; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Collection; + +public interface ContactService { + Collection listContactsOf(UmbrellaUser user) throws UmbrellaException; +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java b/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java index c14e707..e79e7ab 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java @@ -25,6 +25,10 @@ public class UmbrellaException extends Exception{ return this; } + public static UmbrellaException databaseException(String message, Object fills) { + return new UmbrellaException(HTTP_SERVER_ERROR,message,fills); + } + public static UmbrellaException invalidFieldException(String field,String expected){ return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_INVALID_FIELD, field, expected); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java index 9cf7e34..7884dbc 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java @@ -1,15 +1,19 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; +import static de.srsoftware.tools.Optionals.emptyIfNull; import static de.srsoftware.umbrella.core.Constants.*; +import static java.util.Map.entry; +import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Map; import org.json.JSONException; import org.json.JSONObject; -public record Company(long id, String name, String address, String court, String taxId, String phone, String decimalSeparator, String thousandsSeparator, long lastCustomerNumber, int decimals, String customerNumberPrefix, String currency, String email, String bankAccount) { +public record Company(long id, String name, String address, String court, String taxId, String phone, String decimalSeparator, String thousandsSeparator, long lastCustomerNumber, int decimals, String customerNumberPrefix, String currency, String email, String bankAccount) implements Mappable { public static Company of(JSONObject json) throws UmbrellaException { try { @@ -54,4 +58,29 @@ public record Company(long id, String name, String address, String court, String throw new UmbrellaException(500,"Failed to convert ResultSet to Company!").causedBy(e); } } + + @Override + public String toString() { + return name; + } + + @Override + public Map toMap() { + return Map.ofEntries( + entry(ID,id), + entry(NAME,name), + entry(ADDRESS,emptyIfNull(address)), + entry(FIELD_COURT,emptyIfNull(court)), + entry(FIELD_TAX_NUMBER,emptyIfNull(taxId)), + entry(FIELD_PHONE,emptyIfNull(phone)), + entry(DECIMAL_SEPARATOR,emptyIfNull(decimalSeparator)), + entry(THOUSANDS_SEPARATOR,emptyIfNull(thousandsSeparator)), + entry(LAST_CUSTOMER_NUMBER,lastCustomerNumber), + entry(DECIMALS,decimals), + entry(CUSTOMER_NUMBER_PREFIX,emptyIfNull(customerNumberPrefix)), + entry(FIELD_CURRENCY,emptyIfNull(currency)), + entry(EMAIL,emptyIfNull(email)), + entry(FIELD_BANK_ACCOUNT,emptyIfNull(bankAccount)) + ); + } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java new file mode 100644 index 0000000..f56f9c8 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java @@ -0,0 +1,21 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.model; + +import static de.srsoftware.umbrella.core.Constants.DATA; +import static de.srsoftware.umbrella.core.Constants.ID; + +import de.srsoftware.tools.Mappable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +public record Contact(long id, String vcard) implements Mappable { + public static Contact of(ResultSet rs) throws SQLException { + return new Contact(rs.getLong(ID),rs.getString(DATA)); + } + + @Override + public Map toMap() { + return Map.of(); + } +} diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java index b8221f5..8098c42 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java @@ -21,13 +21,13 @@ import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.api.CompanyService; import de.srsoftware.umbrella.core.api.UserService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Company; import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.documents.model.*; import java.io.IOException; import java.time.LocalDate; import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.json.JSONArray; @@ -92,7 +92,7 @@ public class DocumentApi extends BaseHandler { } private boolean getCompanies(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException { - return sendContent(ex,getLegacyCompanies(ex,user,token)); + return sendContent(ex,companies.listCompaniesOf(user).stream().map(Company::toMap)); } private boolean getContacts(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException { @@ -120,14 +120,6 @@ public class DocumentApi extends BaseHandler { return sendContent(ex,doc.renderToMap()); } - private HashMap> getLegacyCompanies(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException { - var location = config.get("umbrella.modules.company.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.company.baseUrl not configured!")); - var resp = request(location, token.asMap(),MIME_FORM_URL,null); - if (!(resp instanceof JSONObject json)) throw new UmbrellaException(500,"{0} did not return JSON!",location); - var result = new HashMap>(); - for (var key : json.keySet()) result.put(Long.parseLong(key),json.getJSONObject(key).toMap()); - return result; - } private JSONArray getLegacyContacts(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException { var location = config.get("umbrella.modules.contact.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.contact.baseUrl not configured!")); var resp = request(location, token.asMap(),MIME_FORM_URL,null); @@ -140,9 +132,8 @@ public class DocumentApi extends BaseHandler { var json = json(ex); if (!json.has(COMPANY)) throw missingFieldException(COMPANY); long companyId = json.getLong(COMPANY); - var companies = getLegacyCompanies(ex,user, token); var company = companies.get(companyId); - if (company == null) return forbidden(ex); + if (!companies.membership(companyId,user.id())) throw new UmbrellaException(HTTP_FORBIDDEN,"You are mot a member of company {0}",company); var docs = db.listDocs(companyId); var map = new HashMap(); for (var entry : docs.entrySet()) map.put(entry.getKey(),entry.getValue().summary()); @@ -159,10 +150,7 @@ public class DocumentApi extends BaseHandler { if (!senderData.has(FIELD_COMPANY) || !(senderData.get(FIELD_COMPANY) instanceof Number companyId)) throw missingFieldException(FIELD_COMPANY); var company = companies.get(companyId.longValue()); - var members = companies.getMembers(companyId.longValue()); - var isMember = false; - for (var member : members) isMember |= user.equals(member); - if (!isMember) return sendContent(ex,HTTP_FORBIDDEN,"You are mot a member of company "+companyId); + if (!companies.membership(companyId.longValue(),user.id())) throw new UmbrellaException(HTTP_FORBIDDEN,"You are mot a member of company {0}",company); if (!json.has(FIELD_CUSTOMER) || !(json.get(FIELD_CUSTOMER) instanceof JSONObject customerData)) throw missingFieldException(FIELD_CUSTOMER); if (!json.has(FIELD_TYPE) || !(json.get(FIELD_TYPE) instanceof Number docTypeId)) throw missingFieldException(FIELD_TYPE); diff --git a/frontend/src/routes/document/Add.svelte b/frontend/src/routes/document/Add.svelte index 878422c..902b41f 100644 --- a/frontend/src/routes/document/Add.svelte +++ b/frontend/src/routes/document/Add.svelte @@ -24,7 +24,9 @@ var resp = await fetch(url,{ credentials: 'include'}); if (resp.ok){ const companies = await resp.json(); - company = companies[document.sender.company]; + for (let c of companies) { + if (c.id == document.sender.company) company = c; + } document.sender.name = ''; if (company.name) document.sender.name += company.name+"\n"; if (company.address) document.sender.name += company.address+"\n"; diff --git a/frontend/src/routes/document/StateSelector.svelte b/frontend/src/routes/document/StateSelector.svelte new file mode 100644 index 0000000..d827f62 --- /dev/null +++ b/frontend/src/routes/document/StateSelector.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/frontend/src/routes/document/View.svelte b/frontend/src/routes/document/View.svelte index 7526391..5014194 100644 --- a/frontend/src/routes/document/View.svelte +++ b/frontend/src/routes/document/View.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { t } from '../../translations.svelte.js'; import { useTinyRouter } from 'svelte-tiny-router'; - + import StateSelector from './StateSelector.svelte'; let { id } = $props(); let error = null; let doc = $state(null); @@ -20,13 +20,67 @@ onMount(loadDoc); +{#if error} +{error} +{/if} -
- {#if error} - {error} - {/if} - {#if doc} - {t('document.type_'+doc.type)} {doc.number} - - {/if} -
+{#if doc} +
+ {t('document.customer')} +
+ {#each doc.customer.name.split("\n") as line} + {line}
+ {/each} +
+
+ {t('document.customer_number',doc.customer.id)} +
+
+ {t('document.tax_id',doc.customer.tax_id)} +
+
+ {t('document.email',doc.customer.email)} +
+
+
+ {t('document.sender')} +
+ {#each doc.sender.name.split("\n") as line} + {line}
+ {/each} +
+
+ {t('document.court',doc.sender.court)} +
+
+ {t('document.tax_id',doc.sender.tax_id)} +
+
+ {t('document.bank_account',doc.sender.bank_account)} +
+
+
+ {t('document.type_'+doc.type)} +
{t('document.number')}: {doc.number}
+
{t('document.state')}:
+
{t('document.date')}: {doc.date}
+
{t('document.delivery')}: {doc.delivery}
+
{t('document.template')}: {doc.template.name} SElektor hier!
+
+
+ {t('document.head')} + {doc.head} +
+
+ {t('document.positions')} + laden! +
+
+ {t('document.footer')} + {doc.footer} +
+
+ {t('document.actions')} + laden! +
+{/if} diff --git a/settings.gradle.kts b/settings.gradle.kts index e2f9bfe..c622015 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,5 @@ include("translations") include("user") include("web") -include("company") \ No newline at end of file +include("company") +include("contact") \ No newline at end of file diff --git a/translations/src/main/resources/de.json b/translations/src/main/resources/de.json index 98fc286..1a3756e 100644 --- a/translations/src/main/resources/de.json +++ b/translations/src/main/resources/de.json @@ -2,9 +2,13 @@ "document": { "actions": "Aktionen", "add_new": "{0} anlegen", + "bank_account": "Bankverbindung: {0}", + "court": "Amtsgericht: {0}", "create_new": "neues Dokument", "customer": "Kunde", + "customer_number": "Kundennummer: {0}", "date": "Datum", + "email": "E-Mail: {0}", "gross_sum": "Brutto-Summe", "list": "Dokumente", "list_of": "Dokumente von {0}", @@ -17,6 +21,7 @@ "state_new":"neu", "state_payed": "bezahlt", "state_sent": "versendet", + "tax_id": "Steuernummer: {0}", "type": "Dokumententyp", "type_confirmation": "Bestätigung", "type_invoice": "Rechnung",