Browse Source

Add Claim, file

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui 1 year ago
parent
commit
f986eb9ffc
32 changed files with 2763 additions and 1 deletions
  1. +1021
    -0
      src/main/java/com/ffii/core/utils/DateUtils.java
  2. +111
    -0
      src/main/java/com/ffii/core/utils/FileUtils.java
  3. +461
    -0
      src/main/java/com/ffii/core/utils/NumberUtils.java
  4. +73
    -0
      src/main/java/com/ffii/core/utils/StringUtils.java
  5. +29
    -0
      src/main/java/com/ffii/core/view/AbstractView.java
  6. +46
    -0
      src/main/java/com/ffii/tsms/modules/claim/entity/Claim.kt
  7. +47
    -0
      src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetail.kt
  8. +7
    -0
      src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetailRepository.kt
  9. +9
    -0
      src/main/java/com/ffii/tsms/modules/claim/entity/ClaimRepository.kt
  10. +92
    -0
      src/main/java/com/ffii/tsms/modules/claim/service/ClaimService.kt
  11. +29
    -0
      src/main/java/com/ffii/tsms/modules/claim/web/ClaimController.kt
  12. +36
    -0
      src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimRequest.kt
  13. +8
    -0
      src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimResponse.kt
  14. +0
    -1
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  15. +39
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/File.kt
  16. +24
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/FileBlob.kt
  17. +9
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/FileBlobRepository.kt
  18. +31
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/FileRef.kt
  19. +6
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/FileRefRepository.kt
  20. +6
    -0
      src/main/java/com/ffii/tsms/modules/file/entity/FileRepository.kt
  21. +326
    -0
      src/main/java/com/ffii/tsms/modules/file/service/FileService.kt
  22. +133
    -0
      src/main/java/com/ffii/tsms/modules/file/web/DownloadFileContoller.kt
  23. +67
    -0
      src/main/java/com/ffii/tsms/modules/file/web/FileController.kt
  24. +45
    -0
      src/main/java/com/ffii/tsms/modules/file/web/UploadFileController.kt
  25. +10
    -0
      src/main/java/com/ffii/tsms/modules/file/web/model/FileReq.kt
  26. +8
    -0
      src/main/java/com/ffii/tsms/modules/file/web/model/UpdateFileReq.kt
  27. +11
    -0
      src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java
  28. +13
    -0
      src/main/resources/db/changelog/changes/20240419_01_cyril/01_update_claim.sql
  29. +50
    -0
      src/main/resources/db/changelog/changes/20240419_01_cyril/02_update_claim.sql
  30. +6
    -0
      src/main/resources/db/changelog/changes/20240419_01_cyril/03_update_claim.sql
  31. +5
    -0
      src/main/resources/db/changelog/changes/20240419_01_cyril/04_update_claim_detail.sql
  32. +5
    -0
      src/main/resources/db/changelog/changes/20240424_01_cyril/01_update_claim.sql

+ 1021
- 0
src/main/java/com/ffii/core/utils/DateUtils.java
File diff suppressed because it is too large
View File


+ 111
- 0
src/main/java/com/ffii/core/utils/FileUtils.java View File

@@ -0,0 +1,111 @@
package com.ffii.core.utils;

import java.util.HashMap;
import java.util.Map;

/**
* File Utils
*
* @author Patrick
*/
public abstract class FileUtils {

private static final Map<String, String> MIMETYPES = new HashMap<>();

static {
MIMETYPES.put("pdf", "application/pdf");

MIMETYPES.put("doc", "application/msword");
MIMETYPES.put("dot", "application/msword");
MIMETYPES.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");

MIMETYPES.put("xls", "application/vnd.ms-excel");
MIMETYPES.put("xlm", "application/vnd.ms-excel");
MIMETYPES.put("xla", "application/vnd.ms-excel");
MIMETYPES.put("xlc", "application/vnd.ms-excel");
MIMETYPES.put("xlt", "application/vnd.ms-excel");
MIMETYPES.put("xlw", "application/vnd.ms-excel");
MIMETYPES.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

MIMETYPES.put("ppt", "application/vnd.ms-powerpoint");
MIMETYPES.put("pps", "application/vnd.ms-powerpoint");
MIMETYPES.put("pot", "application/vnd.ms-powerpoint");
MIMETYPES.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");

MIMETYPES.put("bat", "application/x-msdownload");
MIMETYPES.put("com", "application/x-msdownload");
MIMETYPES.put("dll", "application/x-msdownload");
MIMETYPES.put("exe", "application/x-msdownload");
MIMETYPES.put("msi", "application/x-msdownload");

MIMETYPES.put("swf", "application/x-shockwave-flash");

MIMETYPES.put("7z", "application/x-7z-compressed");
MIMETYPES.put("rar", "application/x-rar-compressed");
MIMETYPES.put("zip", "application/zip");

MIMETYPES.put("js", "application/javascript");
MIMETYPES.put("json", "application/json");

MIMETYPES.put("mpga", "audio/mpeg");
MIMETYPES.put("mp2", "audio/mpeg");
MIMETYPES.put("mp2a", "audio/mpeg");
MIMETYPES.put("mp3", "audio/mpeg");
MIMETYPES.put("m2a", "audio/mpeg");
MIMETYPES.put("m3a", "audio/mpeg");

MIMETYPES.put("bmp", "image/bmp");
MIMETYPES.put("gif", "image/gif");
MIMETYPES.put("jpeg", "image/jpeg");
MIMETYPES.put("jpg", "image/jpeg");
MIMETYPES.put("jpe", "image/jpeg");
MIMETYPES.put("png", "image/png");
MIMETYPES.put("tiff", "image/tiff");
MIMETYPES.put("tif", "image/tiff");

MIMETYPES.put("css", "text/css");

MIMETYPES.put("csv", "text/csv");

MIMETYPES.put("html", "text/html");
MIMETYPES.put("htm", "text/html");

MIMETYPES.put("txt", "text/plain");
MIMETYPES.put("text", "text/plain");
MIMETYPES.put("conf", "text/plain");
MIMETYPES.put("log", "text/plain");

MIMETYPES.put("mp4", "video/mp4");
MIMETYPES.put("mp4v", "video/mp4");
MIMETYPES.put("mpg4", "video/mp4");

MIMETYPES.put("mpeg", "video/mpeg");
MIMETYPES.put("mpg", "video/mpeg");
MIMETYPES.put("mpe", "video/mpeg");
MIMETYPES.put("m1v", "video/mpeg");
MIMETYPES.put("m2v", "video/mpeg");

MIMETYPES.put("qt", "video/quicktime");
MIMETYPES.put("mov", "video/quicktime");

MIMETYPES.put("wmv", "video/x-ms-wmv");
MIMETYPES.put("wmx", "video/x-ms-wmx");
MIMETYPES.put("wvx", "video/x-ms-wvx");
MIMETYPES.put("avi", "video/x-msvideo");

// MIMETYPES.put("xxxxx", "xxxxx");
}

/**
* Guess the mimetype from the file name extension
*
* @return The mimetype guessed from the file name extension, or {@code null} if the mimetype cannot be determined
*/
public static String guessMimetype(String filename) {
String extension = StringUtils.substringAfterLast(filename, ".");
String mimetype = MIMETYPES.get(extension);
return mimetype != null ? mimetype : "application/octet-stream";
}

}


+ 461
- 0
src/main/java/com/ffii/core/utils/NumberUtils.java View File

@@ -0,0 +1,461 @@
package com.ffii.core.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;

import org.apache.commons.lang3.ArrayUtils;

/**
* NumberUtils extends from Apache Commons, and incl some methods from MathUtils
*
* @author Patrick
*/
public abstract class NumberUtils extends org.apache.commons.lang3.math.NumberUtils {

/**
* Round the given value to the specified number of decimal places. The value is rounded using the given method which is any method defined in
* {@link BigDecimal}.
*
* @param x
* the value to round
* @param scale
* the number of digits to the right of the decimal point
* @param roundingMethod
* the rounding method as defined in {@link RoundingMode}
* @return the rounded value
*/
public static double round(double x, int scale, RoundingMode roundingMethod) {
try {
return BigDecimal.valueOf(x).setScale(scale, roundingMethod).doubleValue();
} catch (NumberFormatException ex) {
if (Double.isInfinite(x)) {
return x;
} else {
return Double.NaN;
}
}
}

/**
* Round the given value to the specified number of decimal places. The value is rounded using the {@link RoundingMode#HALF_UP} method.
*
* @param x
* the value to round
* @param scale
* the number of digits to the right of the decimal point
* @return the rounded value
* @see org.apache.commons.math.util.MathUtils#round(double, int)
*/
public static double round(double x, int scale) {
return round(x, scale, RoundingMode.HALF_UP);
}

/**
* Round up the given value to the specified number of decimal places. The value is rounded up using the {@link RoundingMode#UP} method.
*
* @param x
* the value to round up
* @param scale
* the number of digits to the right of the decimal point
* @return the rounded up value
*/
public static double roundUp(double x, int scale) {
return round(x, scale, RoundingMode.UP);
}

/**
* Round down the given value to the specified number of decimal places. The value is rounded down using the {@link RoundingMode#DOWN} method.
*
* @param x
* the value to round down
* @param scale
* the number of digits to the right of the decimal point
* @return the rounded down value
*/
public static double roundDown(double x, int scale) {
return round(x, scale, RoundingMode.DOWN);
}

/**
* Return the {@code int} value of an {@code Object}, or the default value if the object is either {@code null} or not an instance of {@code Number}.
*
* @param obj
* the {@code Object}
* @param defaultValue
* the default value
* @return the {@code int} value of the {@code Object}, or the default if the object is either {@code null} or not an instance of {@code Number}
* @see Number#intValue()
*/
public static int intValue(Object obj, int defaultValue) {
if (obj instanceof Number) {
return ((Number) obj).intValue();
} else if (obj instanceof String) {
try {
return Integer.parseInt((String) obj);
} catch (NumberFormatException nfe) {
}
}
return defaultValue;
}

/**
* Return the {@code int} value of an {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number}.
*
* @param obj
* the {@code Object}
* @return the {@code int} value of the {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number}
* @see Number#intValue()
*/
public static int intValue(Object obj) {
return intValue(obj, 0);
}

/**
* Convert an {@code Object} to an {@code Integer} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
* conversion fails.
* <p>
* If the object is {@code null}, the default value is returned.
*
* @param obj
* the object to convert, may be {@code null}
* @param defaultValue
* the default value, may be {@code null}
* @return the Integer represented by the object, or the default if conversion fails
*/
public static Integer toInt(Object obj, Integer defaultValue) {
if (obj instanceof Number)
return Integer.valueOf(((Number) obj).intValue());
else if (obj instanceof String)
try {
return Integer.valueOf((String) obj);
} catch (NumberFormatException nfe) {
return defaultValue;
}
else
return defaultValue;
}

/**
* Return the {@code long} value of a {@code Long} object, or a default value if the object is {@code null}.
*
* @param obj
* the object (can be {@code null})
* @param defaultValue
* the default value
* @return the {@code long} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN}
* @see Long#longValue()
*/
public static long longValue(Object obj, long defaultValue) {
return (obj instanceof Number) ? ((Number) obj).longValue() : defaultValue;
}

/**
* Return the {@code long} value of a {@code Long} object, or {@code zero} if the object is {@code null}.
*
* @param obj
* the object (can be {@code null})
* @return the {@code long} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN}
* @see Long#longValue()
*/
public static long longValue(Object obj) {
return longValue(obj, 0l);
}

/**
* Convert an {@code Object} to a {@code Long} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
* conversion fails.
* <p>
* If the object is {@code null}, the default value is returned.
*
* @param obj
* the object to convert, may be {@code null}
* @param defaultValue
* the default value, may be {@code null}
* @return the Long represented by the object, or the default if conversion fails
*/
public static Long toLong(Object obj, Long defaultValue) {
if (obj instanceof Number)
return Long.valueOf(((Number) obj).longValue());
else if (obj instanceof String)
try {
return Long.valueOf((String) obj);
} catch (NumberFormatException nfe) {
return defaultValue;
}
else
return defaultValue;
}

/**
* @param obj
* the object (can be {@code null})
* @param defaultValue
* the default value
* @return the {@code double} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN}
* @see Double#doubleValue()
*/
public static double doubleValue(Object obj, double defaultValue) {
return (obj instanceof Number) ? ((Number) obj).doubleValue() : defaultValue;
}

/**
* @param obj
* the object (can be {@code null})
* @param defaultValue
* the default value
* @return the {@code double} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN}
* @see Double#doubleValue()
*/
public static double doubleValue(Object obj) {
return doubleValue(obj, 0.0d);
}

/**
* Convert an {@code Object} to a {@code Double} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
* conversion fails.
* <p>
* If the object is {@code null}, the default value is returned.
*
* @param obj
* the object to convert, may be {@code null}
* @param defaultValue
* the default value, may be {@code null}
* @return the Double represented by the object, or the default if conversion fails
*/
public static Double toDouble(Object obj, Double defaultValue) {
if (obj instanceof Number)
return Double.valueOf(((Number) obj).doubleValue());
else if (obj instanceof String)
try {
return Double.valueOf((String) obj);
} catch (NumberFormatException nfe) {
return defaultValue;
}
else
return defaultValue;
}

/**
* Return the {@code BigDecimal} object, or {@code zero} if the object is {@code null}.
*
* @param obj
* the {@code BigDecimal} object
* @return the {@code BigDecimal} object, or {@code zero} if the object is {@code null}
*/
public static BigDecimal decimalValue(BigDecimal obj) {
return decimalValue(obj, BigDecimal.ZERO);
}

/**
* Return the {@code BigDecimal} object, or a default value if the object is {@code null}.
*
* @param obj
* the {@code BigDecimal} object
* @param defaultValue
* the default value
* @return the {@code BigDecimal} object, or the default if the object is {@code null}
*/
public static BigDecimal decimalValue(BigDecimal obj, BigDecimal defaultValue) {
return obj == null ? defaultValue : obj;
}

/**
* Convert an {@code Object} to a {@code BigDecimal}, returning {@code BigDecimal.ZERO} if the conversion fails (e.g. the object is not an instance of
* {@code Number} nor {@code String}).
* <p>
* If the object is {@code null}, {@code BigDecimal.ZERO} is returned.
*
* @param obj
* the object to convert, may be {@code null}
* @return the BigDecimal represented by the object, or {@code BigDecimal.ZERO} if conversion fails
*/
public static BigDecimal toDecimal(Object obj) {
return toDecimal(obj, BigDecimal.ZERO);
}

/**
* Convert an {@code Object} to a {@code BigDecimal}, returning a default value if the conversion fails (e.g. the object is not an instance of
* {@code Number} nor {@code String}).
* <p>
* If the object is {@code null}, the default value is returned.
*
* @param obj
* the object to convert, may be {@code null}
* @param defaultValue
* the default value, may be {@code null}
* @return the BigDecimal represented by the object, or the default if conversion fails
*/
public static BigDecimal toDecimal(Object obj, BigDecimal defaultValue) {
if (obj instanceof BigDecimal)
return (BigDecimal) obj;
else if (obj instanceof Number)
return BigDecimal.valueOf(((Number) obj).doubleValue());
else if (obj instanceof String)
try {
return new BigDecimal((String) obj);
} catch (NumberFormatException nfe) {
return defaultValue;
}
else
return defaultValue;
}

/**
* Null-safe method to check if the two {@code Integer} objects have the same value.
*
* <ol>
* <li>Returns {@code true} if {@code a} and {@code b} are both {@code null}.
* <li>Returns {@code false} if only one of them is {@code null}.
* <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code int} value, else returns {@code false}.
* </ol>
*
* @param a
* Integer obj, may be {@code null}
* @param b
* Integer obj, may be {@code null}
*/
public static boolean isEqual(Integer a, Integer b) {
return a == null ? (b == null ? true : false) : (b == null ? false : a.equals(b));
}

/**
* Null-safe method to check if the two {@code Integer} objects have different values.
*
* <ol>
* <li>Returns {@code false} if {@code a} and {@code b} are both {@code null}.
* <li>Returns {@code true} if only one of them is {@code null}.
* <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code int} values, else returns {@code false}.
* </ol>
*
* @param a
* Integer obj, may be {@code null}
* @param b
* Integer obj, may be {@code null}
*/
public static boolean isNotEqual(Integer a, Integer b) {
return !isEqual(a, b);
}

/**
* Null-safe method to check if the two {@code BigDecimal} objects have the same value.
* <p>
* Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method.
*
* <ol>
* <li>Returns {@code true} if {@code a} and {@code b} are both {@code null}.
* <li>Returns {@code false} if only one of them is {@code null}.
* <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code decimal} value, else returns {@code false}.
* </ol>
*
* @param a
* BigDecimal obj, may be {@code null}
* @param b
* BigDecimal obj, may be {@code null}
*/
public static boolean isEqual(BigDecimal a, BigDecimal b) {
return a == null ? (b == null ? true : false) : (b == null ? false : a.compareTo(b) == 0);
}

/**
* Null-safe method to check if the two {@code BigDecimal} objects have different values.
* <p>
* Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method.
*
* <ol>
* <li>Returns {@code false} if {@code a} and {@code b} are both {@code null}.
* <li>Returns {@code true} if only one of them is {@code null}.
* <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code decimal} values, else returns {@code false}.
* </ol>
*
* @param a
* BigDecimal obj, may be {@code null}
* @param b
* BigDecimal obj, may be {@code null}
*/
public static boolean isNotEqual(BigDecimal a, BigDecimal b) {
return !isEqual(a, b);
}

/**
* Check if {@code BigDecimal} object {@code a} is greater than {@code BigDecimal} object {@code b}.
*
* @param a
* non-{@code null} BigDecimal obj
* @param b
* non-{@code null} BigDecimal obj
*/
public static boolean isGreaterThan(BigDecimal a, BigDecimal b) {
return a.compareTo(b) > 0;
}

/**
* Check if {@code BigDecimal} object {@code a} is greater than or equals to {@code BigDecimal} object {@code b}.
*
* @param a
* non-{@code null} BigDecimal obj
* @param b
* non-{@code null} BigDecimal obj
*/
public static boolean isGreaterThanOrEqual(BigDecimal a, BigDecimal b) {
return a.compareTo(b) >= 0;
}

/**
* Check if {@code BigDecimal} object {@code a} is less than {@code BigDecimal} object {@code b}.
*
* @param a
* non-{@code null} BigDecimal obj
* @param b
* non-{@code null} BigDecimal obj
*/
public static boolean isLessThan(BigDecimal a, BigDecimal b) {
return a.compareTo(b) < 0;
}

/**
* Check if {@code BigDecimal} object {@code a} is less than or equals to {@code BigDecimal} object {@code b}.
*
* @param a
* non-{@code null} BigDecimal obj
* @param b
* non-{@code null} BigDecimal obj
*/
public static boolean isLessThanOrEqual(BigDecimal a, BigDecimal b) {
return a.compareTo(b) <= 0;
}

/**
*
* <pre>
* NumberUtils.equalsAny(null, (Integer[]) null) = false
* NumberUtils.equalsAny(null, null, null) = true
* NumberUtils.equalsAny(null, 1, 2) = false
* NumberUtils.equalsAny(1, null, 2) = false
* NumberUtils.equalsAny(1, 1, 2) = true
* </pre>
*
* @param int
* to compare, may be {@code null}.
* @param searchInts
* a int, may be {@code null}.
* @return {@code true} if the num is equal to any other element of <code>searchInts</code>; {@code false} if <code>searchInts</code> is null or contains no
* matches.
*/
public static boolean equalsAny(final int num, int... searchInts) {
if (ArrayUtils.isNotEmpty(searchInts)) {
for (int next : searchInts) {
if (num == next) {
return true;
}
}
}
return false;
}

public static double sum(double... nums) {
BigDecimal rs = BigDecimal.ZERO;
for (double num : nums)
rs = rs.add(BigDecimal.valueOf(num));
return rs.doubleValue();
}
}

+ 73
- 0
src/main/java/com/ffii/core/utils/StringUtils.java View File

@@ -0,0 +1,73 @@
package com.ffii.core.utils;

/**
* String Utils based on Apache Commons StringUtils.
*
* @author Patrick
*/
public abstract class StringUtils extends org.apache.commons.lang3.StringUtils {

/**
* The String {@code "0"}.
*/
public static final String ZERO = "0";

/**
* The String {@code "1"}.
*/
public static final String ONE = "1";

/**
* The String {@code "%"}.
*/
public static final String PERCENT = "%";

/**
* The String {@code ","}.
*/
public static final String COMMA = ",";

/**
* The String {@code "\r\n"} for line break on Windows
*/
public static final String LINE_BREAK_WINDOWS = "\r\n";

/**
* The String {@code "\n"} for line break on Unix/Linux
*/
public static final String LINE_BREAK_LINUX = "\n";

public static final String[] A2Z_LOWWER = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z" };
public static final String[] A2Z_UPPER = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z" };

public static final String concat(String segregator, final String... chars) {
if (segregator == null)
segregator = "";
String rs = "";
for (String c : chars) {
if (c == null)
continue;
else {
if (StringUtils.isBlank(rs)) {
rs = c;
} else {
rs += segregator + c;
}
}
}
return rs;
}

public static final String removeLineBreak(String str) {
if (str == null)
return str;
return str.replace("\r\n", " ")
.replace("\n", " ")
.replace("\r", " ")
.trim();
}
}

+ 29
- 0
src/main/java/com/ffii/core/view/AbstractView.java View File

@@ -0,0 +1,29 @@
package com.ffii.core.view;

public abstract class AbstractView extends org.springframework.web.servlet.view.AbstractView {

public static final String CONTENT_TYPE_JSON = "application/json";

public static final String CONTENT_TYPE_JAVASCRIPT = "text/javascript";

public static final String CONTENT_TYPE_TEXT_HTML = "text/html";
public static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain";
public static final String CONTENT_TYPE_XML = "text/xml";

public static final String CONTENT_TYPE_JPEG = "image/jpeg";
public static final String CONTENT_TYPE_PNG = "image/png";

public static final String CHARSET_UTF8 = "UTF-8";

protected boolean disableCaching = true;

/**
* Disables caching of the generated JSON. <br>
* Default is {@code true}, which will prevent the client from caching the generated JSON.
*/
public void setDisableCaching(boolean disableCaching) {
this.disableCaching = disableCaching;
}

}


+ 46
- 0
src/main/java/com/ffii/tsms/modules/claim/entity/Claim.kt View File

@@ -0,0 +1,46 @@
package com.ffii.tsms.modules.claim.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.core.entity.IdEntity
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.file.entity.File
import com.ffii.tsms.modules.project.entity.Project
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import java.math.BigDecimal
import java.time.LocalDateTime

@Entity
@Table(name = "claim")
open class Claim : BaseEntity<Long>() {

@NotNull
@ManyToOne
@JoinColumn(name = "staffId")
open var staff: Staff? = null

@Column(name = "code")
open var code: String? = null

@NotNull
@Column(name = "type")
open var type: String? = null

@NotNull
@Column(name = "status")
open var status: String? = null

@Column(name = "verifiedDatetime")
open var verifiedDatetime: LocalDateTime? = null

@Column(name = "verifiedBy")
open var verifiedBy: Long? = null

@Column(name = "remark", length = 255)
open var remark: String? = null
}

+ 47
- 0
src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetail.kt View File

@@ -0,0 +1,47 @@
package com.ffii.tsms.modules.claim.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.file.entity.File
import com.ffii.tsms.modules.project.entity.Project
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime

@Entity
@Table(name = "claim_detail")
open class ClaimDetail : BaseEntity<Long>() {

@NotNull
@ManyToOne
@JoinColumn(name = "claimId")
open var claim: Claim? = null

@NotNull
@Column(name = "invoiceDate")
open var invoiceDate: LocalDate? = null

@NotNull
@ManyToOne
@JoinColumn(name = "projectId")
open var project: Project? = null

@NotNull
@Column(name = "description", length = 255)
open var description: String? = null

@NotNull
@Column(name = "amount")
open var amount: BigDecimal? = null

@OneToOne
@JoinColumn(name = "fileId")
open var file: File? = null
}

+ 7
- 0
src/main/java/com/ffii/tsms/modules/claim/entity/ClaimDetailRepository.kt View File

@@ -0,0 +1,7 @@
package com.ffii.tsms.modules.claim.entity

import com.ffii.core.support.AbstractRepository

interface ClaimDetailRepository : AbstractRepository<ClaimDetail, Long> {
fun findAllByDeletedFalse(): List<ClaimDetail>
}

+ 9
- 0
src/main/java/com/ffii/tsms/modules/claim/entity/ClaimRepository.kt View File

@@ -0,0 +1,9 @@
package com.ffii.tsms.modules.claim.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.data.repository.query.Param

interface ClaimRepository : AbstractRepository<Claim, Long> {
fun findAllByDeletedFalse(): List<Claim>
fun findAllByCodeContains(@Param("code") code:String): List<Claim>
}

+ 92
- 0
src/main/java/com/ffii/tsms/modules/claim/service/ClaimService.kt View File

@@ -0,0 +1,92 @@
package com.ffii.tsms.modules.claim.service

import com.ffii.tsms.modules.claim.entity.Claim
import com.ffii.tsms.modules.claim.entity.ClaimDetail
import com.ffii.tsms.modules.claim.entity.ClaimDetailRepository
import com.ffii.tsms.modules.claim.entity.ClaimRepository
import com.ffii.tsms.modules.claim.web.models.SaveClaimDetailRequest
import com.ffii.tsms.modules.claim.web.models.SaveClaimRequest
import com.ffii.tsms.modules.claim.web.models.SaveClaimResponse
import com.ffii.tsms.modules.file.entity.FileBlob
import com.ffii.tsms.modules.file.service.FileService
import com.ffii.tsms.modules.project.entity.ProjectRepository
import org.springframework.beans.BeanUtils
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.text.NumberFormat
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.HashMap

@Service
open class ClaimService(
private val claimRepository: ClaimRepository,
private val fileService: FileService, private val claimDetailRepository: ClaimDetailRepository, private val projectRepository: ProjectRepository
) {
open fun allClaims(): List<Claim> {
return claimRepository.findAllByDeletedFalse()
}

open fun allClaimsByCodeContains(code: String): List<Claim> {
return claimRepository.findAllByCodeContains(code)
}

open fun generateCode(): String {
val dateFormatter = DateTimeFormatter.ofPattern("yyMMdd")
val formattedToday = LocalDate.now().format(dateFormatter)
val claims = allClaimsByCodeContains(formattedToday)

// Format: CF-yymmddXXX
return "CF-" + formattedToday + String.format("%03d", (claims.size + 1))
}

@Transactional(rollbackFor = [Exception::class])
open fun saveClaimDetail(claimDetail: SaveClaimDetailRequest, claim: Claim) {
val oldSupportingDocumentId = claimDetail.oldSupportingDocument?.id // fileId
val newSupportingDocument = claimDetail.newSupportingDocument
val claimDetailId = claimDetail.id
val projectId = claimDetail.project.id
var result: MutableMap<String, Any> = HashMap<String, Any>()

if (oldSupportingDocumentId != null && oldSupportingDocumentId > 0 && newSupportingDocument != null) {
result = fileService.updateFile(multipartFile = newSupportingDocument, refId = claim.id, refType = "claimDetail", refCode = claim.code, maxSize = 50, fileId = oldSupportingDocumentId)
} else if ((oldSupportingDocumentId == null || oldSupportingDocumentId <= 0) && newSupportingDocument != null) {
result = fileService.uploadFile(multipartFile = newSupportingDocument, refId = claim.id, refType = "claimDetail", refCode = claim.code, maxSize = 50)
}

val instance = claimDetailRepository.findById(claimDetailId).orElse(ClaimDetail())
BeanUtils.copyProperties(claimDetail, instance)

val project = projectRepository.findById(projectId!!).orElseThrow()
instance.apply {
this.project = project
}

claimDetailRepository.save(instance)
}

@Transactional(rollbackFor = [Exception::class])
open fun saveClaim(saveClaimRequest: SaveClaimRequest): SaveClaimResponse {

val claimId = saveClaimRequest.id
val addClaimDetails = saveClaimRequest.addClaimDetails
// Save to claim
val instance = if (claimId != null && claimId > 0) claimRepository.findById(claimId).orElseThrow() else Claim();

instance.apply {
type = saveClaimRequest.expenseType
code = if(instance.code.isNullOrEmpty()) generateCode() else instance.code
}

claimRepository.save(instance)

// Save to claim detail
if (addClaimDetails.isNotEmpty()) {
for(addClaimDetail in addClaimDetails) {
saveClaimDetail(addClaimDetail, instance)
}
}

return SaveClaimResponse(claim = instance, message = "Success");
}
}

+ 29
- 0
src/main/java/com/ffii/tsms/modules/claim/web/ClaimController.kt View File

@@ -0,0 +1,29 @@
package com.ffii.tsms.modules.claim.web

import com.ffii.tsms.modules.claim.entity.Claim
import com.ffii.tsms.modules.claim.service.ClaimService
import com.ffii.tsms.modules.claim.web.models.SaveClaimRequest
import com.ffii.tsms.modules.claim.web.models.SaveClaimResponse
import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse
import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/claim")
class ClaimController(private val claimService: ClaimService) {

@GetMapping
fun allClaims(): List<Claim> {
return claimService.allClaims()
}

@PostMapping("/save")
fun saveClaim(@Valid @RequestBody saveClaimRequest: SaveClaimRequest): SaveClaimResponse {
return claimService.saveClaim(saveClaimRequest)
}
}

+ 36
- 0
src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimRequest.kt View File

@@ -0,0 +1,36 @@
package com.ffii.tsms.modules.claim.web.models

import com.ffii.tsms.modules.claim.entity.ClaimDetail
import jakarta.validation.constraints.NotBlank
import org.springframework.web.multipart.MultipartFile
import java.math.BigDecimal
import java.time.LocalDate

data class SupportingDocument (
val id: Long,
val skey: String,
val filename: String,
)

data class Project (
val id: Long,
)
data class SaveClaimDetailRequest (
val id: Long,
val invoiceDate: LocalDate,
val description: String,
val project: Project,
val amount: BigDecimal,
val newSupportingDocument: MultipartFile?,
val oldSupportingDocument: SupportingDocument?
)

data class SaveClaimRequest (
@field:NotBlank(message = "Expense type cannot be empty")
val expenseType: String,

val addClaimDetails: List<SaveClaimDetailRequest>,

val status: String,
val id: Long?,
)

+ 8
- 0
src/main/java/com/ffii/tsms/modules/claim/web/models/SaveClaimResponse.kt View File

@@ -0,0 +1,8 @@
package com.ffii.tsms.modules.claim.web.models

import com.ffii.tsms.modules.claim.entity.Claim

data class SaveClaimResponse(
val claim: Claim,
val message: String,
)

+ 0
- 1
src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt View File

@@ -117,7 +117,6 @@ open class StaffsService(
staffRepository.save(staff)

salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.id!!)
logger.info(staff.id)
return staff
}
@Transactional(rollbackFor = [Exception::class])


+ 39
- 0
src/main/java/com/ffii/tsms/modules/file/entity/File.kt View File

@@ -0,0 +1,39 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.core.entity.IdEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import java.math.BigInteger

@Entity
@Table(name = "file")
open class File : BaseEntity<Long>() {

open val DEFAULT_UPLOAD_MAX_FILE_SIZE_MB: Int = 50

@NotNull
@Column(name = "skey", length = 32)
open var skey: String? = null

@NotNull
@Column(name = "filename", length = 255)
open var filename: String? = null

@NotNull
@Column(name = "extension", length = 10)
open var description: String? = null

@NotNull
@Column(name = "mimetype", length = 255)
open var mimetype: String? = null

@NotNull
@Column(name = "filesize")
open var filesize: Long? = null

@Column(name = "remarks", length = 500)
open var remarks: String? = null
}

+ 24
- 0
src/main/java/com/ffii/tsms/modules/file/entity/FileBlob.kt View File

@@ -0,0 +1,24 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.core.entity.IdEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull

@Entity
@Table(name = "file_blob")
open class FileBlob : BaseEntity<Long>() {

@NotNull
@OneToOne
@JoinColumn(name = "fileId")
open var file: File? = null

@NotNull
@Column(name = "bytes")
open var bytes: ByteArray? = null
}

+ 9
- 0
src/main/java/com/ffii/tsms/modules/file/entity/FileBlobRepository.kt View File

@@ -0,0 +1,9 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.data.repository.query.Param
import java.util.Optional

interface FileBlobRepository : AbstractRepository<FileBlob, Long> {

}

+ 31
- 0
src/main/java/com/ffii/tsms/modules/file/entity/FileRef.kt View File

@@ -0,0 +1,31 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.core.entity.IdEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull

@Entity
@Table(name = "file_ref")
open class FileRef : BaseEntity<Long>() {

@NotNull
@Column(name = "refType", length = 20)
open var refType: String? = null

@NotNull
@Column(name = "refId")
open var refId: Long? = null

@Column(name = "refCode")
open var refCode: String? = null

@NotNull
@OneToOne
@JoinColumn(name = "fileId")
open var file: File? = null
}

+ 6
- 0
src/main/java/com/ffii/tsms/modules/file/entity/FileRefRepository.kt View File

@@ -0,0 +1,6 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.support.AbstractRepository

interface FileRefRepository : AbstractRepository<FileRef, Long> {
}

+ 6
- 0
src/main/java/com/ffii/tsms/modules/file/entity/FileRepository.kt View File

@@ -0,0 +1,6 @@
package com.ffii.tsms.modules.file.entity

import com.ffii.core.support.AbstractRepository

interface FileRepository : AbstractRepository<File, Long> {
}

+ 326
- 0
src/main/java/com/ffii/tsms/modules/file/service/FileService.kt View File

@@ -0,0 +1,326 @@
package com.ffii.tsms.modules.file.service

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.FileUtils
import com.ffii.core.utils.NumberUtils
import com.ffii.core.utils.Params
import com.ffii.tsms.modules.file.entity.*
import org.apache.commons.lang3.RandomStringUtils
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
import java.io.ByteArrayInputStream
import java.io.IOException
import java.util.*
import java.util.Map
import javax.imageio.ImageIO


@Service
open class FileService(
private val jdbcDao: JdbcDao,
private val fileRepository: FileRepository,
private val fileRefRepository: FileRefRepository,
private val fileBlobRepository: FileBlobRepository,
) : AbstractBaseEntityService<File, Long, FileRepository>(jdbcDao, fileRepository) {

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun saveFile(instance: File): File {
return fileRepository.save(instance)
}

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun saveFileBlob(instance: FileBlob): FileBlob {
return fileBlobRepository.save(instance)
}

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun saveFileRef(instance: FileRef): FileRef {
return fileRefRepository.save(instance)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun findFileByIdAndKey(id: Long, skey: String): Optional<File> {
return jdbcDao.queryForEntity(
"SELECT * from file f where f.id = :id and f.skey = :skey ",
Map.of("id", id, "skey", skey), File::class.java
)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun findFileRefByTypeAndId(refType: String, refId: Long): List<FileRef> {
return jdbcDao.queryForList(
"SELECT * from file_ref where refType = :refType and refId = :refId order by id ",
java.util.Map.of("refType", refType, "refId", refId), FileRef::class.java
)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun findFileRefByFileId(fileId: Long): Optional<FileRef> {
return jdbcDao.queryForEntity(
"SELECT * from file_ref where fileId = :fileId", java.util.Map.of("fileId", fileId),
FileRef::class.java
)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun findFileBlobByFileId(fileId: Long): Optional<FileBlob> {
return jdbcDao.queryForEntity(
"SELECT * from file_blob fb where fb.fileId = :fileId ",
java.util.Map.of("fileId", fileId), FileBlob::class.java
)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun isFileExists(id: Long?, skey: String?): Boolean {
val sql = ("SELECT"
+ " COUNT(1)"
+ " FROM file f"
+ " WHERE f.deleted = 0"
+ " AND f.id = :id"
+ " AND f.skey = :skey")

val count = jdbcDao.queryForInt(sql, Map.of("id", id, "skey", skey))

return (count > 0)
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = [Exception::class], readOnly = true)
open fun searchFiles(args: MutableMap<String, Any>): MutableList<MutableMap<String, Any>>? {
val sql = StringBuilder(
"SELECT"
+ " f.id,"
+ " f.filename,"
+ " f.filesize,"
+ " f.skey,"
+ " fr.refId,"
+ " fr.refType,"
+ " f.created,"
+ " u.name AS createdByName,"
+ " f.description"
+ " FROM file f"
+ " LEFT JOIN file_ref fr ON f.id = fr.fileId"
+ " LEFT JOIN user u ON f.createdBy = u.id"
+ " WHERE f.deleted = 0"
)

if (args.containsKey("filename")) sql.append(" AND f.filename = :filename")

if (args.containsKey("refType")) sql.append(" AND fr.refType = :refType")

if (args.containsKey("refId")) sql.append(" AND fr.refId = :refId")

if (args.containsKey("startDate")) sql.append(" AND f.created >= :startDate")

if (args.containsKey("endDate")) sql.append(" AND f.created < :endDate")

sql.append(" ORDER BY f.created DESC")

return jdbcDao.queryForList(sql.toString(), args)
}

/**
* Delete `FileRef` by `fileId`, `refId`,
* `refType`, and `skey`
*/
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun deleteFile(fileId: Long, refId: Long, refType: String, skey: String) {
val args =
Map.of("fileId", fileId, "refId", refId, "refType", refType, "skey", skey)

jdbcDao.executeUpdate(
("DELETE FROM file_ref"
+ " WHERE fileId = :fileId"
+ " AND refId = :refId"
+ " AND refType = :refType"
+ " AND EXISTS (SELECT 1 FROM file WHERE id = file_ref.fileId AND skey = :skey)"),
args
)
}

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun deleteFile(fileId: Long?, skey: String?, filename: String?) {
val args = Map.of("fileId", fileId, "skey", skey, "filename", filename)

jdbcDao.executeUpdate(
("DELETE FROM file_ref "
+ " WHERE fileId = :fileId "
+ " AND EXISTS (SELECT 1 FROM file WHERE id = file_ref.fileId AND skey = :skey AND filename = :filename)"),
args
)
}

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = [Exception::class], readOnly = false)
open fun saveFile(
filename: String,
description: String,
refType: String,
refId: Long,
refCode: String,
bytes: ByteArray
): Long {
val file: File = File()

file.apply {
this.filename = filename
this.description = description
mimetype = FileUtils.guessMimetype(filename)
filesize = NumberUtils.toLong(bytes.size, 0L)
skey = RandomStringUtils.randomAlphanumeric(32)
}

val fileBlob: FileBlob = FileBlob()

fileBlob.apply {
this.bytes = bytes
}

val fileRef: FileRef = FileRef()

fileRef.apply {
this.refId = refId
this.refType = refType
this.refCode = refCode
}

// save File
fileRepository.save(file)

// save FileBlob
fileBlob.apply {
this.file = file
}
fileBlobRepository.save(fileBlob)

// save FileRef
fileRef.apply {
this.file = file
}
fileRefRepository.save(fileRef)

return file.id!!
}

@Transactional(rollbackFor = [Exception::class])
@Throws(IOException::class)
open fun uploadFile(
multipartFile: MultipartFile, refId: Long?, refType: String?, refCode: String?,
maxSize: Int
): MutableMap<String, Any> {
val result: MutableMap<String, Any> = HashMap<String, Any>()

// filesize < 50MB
if (multipartFile.size > 0 && multipartFile.size <= maxSize * 1024 * 1024) {
// DEBUG LOG

logger.info("multipartFile.getSize() = " + multipartFile.size)

val file: File = File()

file.apply {
filename = multipartFile.originalFilename
mimetype = FileUtils.guessMimetype(multipartFile.originalFilename)
filesize = multipartFile.size
skey = RandomStringUtils.randomAlphanumeric(32)
}

// get height and width if mimetype is png or jpeg
// if (AbstractView.CONTENT_TYPE_PNG.equals(file.getMimetype())
// || AbstractView.CONTENT_TYPE_JPEG.equals(file.getMimetype())
// ) {
// val image = ImageIO.read(ByteArrayInputStream(fileBlob.getBytes()))
// if (image != null) {
// file.setImageHeight(image.height)
// file.setImageWidth(image.width)
// }
// }

// create UserFile
saveFile(file)

// create UserFileBlob
val fileBlob: FileBlob = FileBlob()
fileBlob.apply {
bytes = multipartFile.bytes
this.file = file
}
saveFileBlob(fileBlob)

// create UserFileRef
val fileRef: FileRef = FileRef()

fileRef.apply {
this.refId = refId
this.refType = refType
this.refCode = refCode
this.file = file
}
saveFileRef(fileRef)

result["fileId"] = file.id!!
result["skey"] = file.skey!!
result["filename"] = file.filename!!
} else {
result["success"] = java.lang.Boolean.FALSE
result[Params.MSG] = "Upload Failed"
}
return result
}

@Transactional(rollbackFor = [Exception::class])
@Throws(IOException::class)
open fun updateFile(
multipartFile: MultipartFile, refId: Long?, refType: String?, refCode: String?,
maxSize: Int, fileId: Long
): MutableMap<String, Any> {
val result: MutableMap<String, Any> = HashMap<String, Any>()

// filesize < 50MB
if (multipartFile.size > 0 && multipartFile.size <= maxSize * 1024 * 1024) {
// DEBUG LOG

logger.info("multipartFile.getSize() = " + multipartFile.size)

val file: File = fileRepository.findById(fileId).orElse(File())

file.apply {
filename = multipartFile.originalFilename
mimetype = FileUtils.guessMimetype(multipartFile.originalFilename)
filesize = multipartFile.size
skey = RandomStringUtils.randomAlphanumeric(32)
}

// create UserFile
saveFile(file)

// create UserFileBlob
val fileBlob: FileBlob = findFileBlobByFileId(fileId).orElse(FileBlob())
fileBlob.apply {
bytes = multipartFile.bytes
this.file = file
}
saveFileBlob(fileBlob)

// create UserFileRef
val fileRef: FileRef = findFileRefByFileId(fileId).orElse(FileRef())

fileRef.apply {
this.refId = refId
this.refType = refType
this.refCode = refCode
this.file = file
}
saveFileRef(fileRef)

result["fileId"] = file.id!!
result["skey"] = file.skey!!
result["filename"] = file.filename!!
} else {
result["success"] = java.lang.Boolean.FALSE
result[Params.MSG] = "Update Failed"
}
return result
}
}

+ 133
- 0
src/main/java/com/ffii/tsms/modules/file/web/DownloadFileContoller.kt View File

@@ -0,0 +1,133 @@
package com.ffii.tsms.modules.file.web

import com.ffii.core.utils.DateUtils
import com.ffii.core.utils.NumberUtils
import com.ffii.core.utils.StringUtils
import com.ffii.tsms.modules.file.entity.File
import com.ffii.tsms.modules.file.entity.FileBlob
import com.ffii.tsms.modules.file.service.FileService
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Autowired
import java.text.DecimalFormat
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.io.IOException
import java.net.URLEncoder
import java.util.Date

@RestController
@RequestMapping("/file")
open class DownloadFileController (private val fileService: FileService) {
private val logger: Log = LogFactory.getLog(javaClass)

private val ZIP_NUMBERER : Boolean = false

@GetMapping("/dl/{id}/{skey}/{filename}")
@Throws(IOException::class)
open fun download(
request: HttpServletRequest, response: HttpServletResponse, @RequestParam(defaultValue = "false") dl: Boolean,
@PathVariable id: Long, @PathVariable skey: String, @PathVariable filename: String
) {
val file: File = fileService.findFileByIdAndKey(id, skey).get()

if (file != null) {
val fileBlob: FileBlob = fileService.findFileBlobByFileId(id).get()

response.reset()
response.contentType = file.mimetype
response.setContentLength(NumberUtils.toInt(file.filesize, 0))
response.setHeader("Content-Transfer-Encoding", "binary")
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
response.setHeader(
"Content-Disposition",
java.lang.String.format(
"%s; filename=\"%s\"",
if (dl) "attachment" else "inline",
URLEncoder.encode(file.filename, "UTF-8")
)
)


// String origin = request.getHeader("Origin");
//TODO: anna Access-Control-Allow-Origin
response.addHeader("Access-Control-Allow-Origin", "*")
response.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET")

val out = response.outputStream

try {
out.write(fileBlob.bytes!!)
out.flush()
out.close()
} catch (e: IOException) {
logger.warn(e.message)
} finally {
out.close()
}
} else {
logger.info("*** 400 BAD REQUEST ***")
response.status = HttpServletResponse.SC_BAD_REQUEST
}
}

@GetMapping(value = ["/dlz"], produces = ["application/zip"])
@Throws(IOException::class)
open fun downloadAsZip(
response: HttpServletResponse,
@RequestParam filename: String, @RequestParam ids: String, @RequestParam skeys: String
) {
var filename = filename
filename += DateUtils.formatDate(Date(), "_yyyy_MM_dd_HHmm", "") + ".zip"
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
response.setHeader("Content-Transfer-Encoding", "binary")
response.setHeader(
"Content-Disposition",
String.format("%s; filename=\"%s\"", "attachment", response.encodeURL(filename))
)

val df00 = DecimalFormat("00")

val zipOutputStream = ZipOutputStream(response.outputStream)

val idsA: Array<String> = StringUtils.split(ids, ',')
val skeysA: Array<String> = StringUtils.split(skeys, ',')

var filePrefixIdx = 1

for (i in idsA.indices) {
val id: Long = NumberUtils.longValue(idsA[i])
val skey = skeysA[i]

val file: File = fileService.findFileByIdAndKey(id, skey).get()

if (file == null) {
logger.warn("*** file is null, id = $id, skey = $skey")
} else {
val fileBlob: FileBlob = fileService.findFileBlobByFileId(id).get()

if (fileBlob == null) {
logger.warn("*** fileBlob is null, id = $id, skey = $skey")
} else {
zipOutputStream.putNextEntry(
ZipEntry(
if (ZIP_NUMBERER) (df00.format(filePrefixIdx++.toLong()) + "_" + file.filename)
else file.filename
)
)
zipOutputStream.write(fileBlob.bytes)
zipOutputStream.closeEntry()
}
}
}
zipOutputStream.flush()
zipOutputStream.close()
}
}

+ 67
- 0
src/main/java/com/ffii/tsms/modules/file/web/FileController.kt View File

@@ -0,0 +1,67 @@
package com.ffii.tsms.modules.file.web

import com.ffii.core.response.RecordsRes
import com.ffii.core.utils.Params
import com.ffii.tsms.modules.file.entity.File
import com.ffii.tsms.modules.file.service.FileService
import com.ffii.tsms.modules.file.web.model.FileReq
import com.ffii.tsms.modules.file.web.model.UpdateFileReq
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid
import org.springframework.ui.Model
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(value = ["/file"])
open class FileController (private val fileService: FileService) {

@PostMapping("/list")
@Throws(ServletRequestBindingException::class)
fun listJson(
model: Model?,
request: HttpServletRequest?,
@RequestBody req: @Valid FileReq
): RecordsRes<Map<String, Any>> {
val args: MutableMap<String, Any> = HashMap()
args["refType"] = req.refType
args["refId"] = req.refId

val records: List<Map<String, Any>>? = fileService.searchFiles(args)

return RecordsRes(records)
}

@PostMapping("/update")
fun update(
model: Model?,
response: HttpServletResponse?,
@RequestBody req: @Valid UpdateFileReq
): Map<String, Any> {
val file: File = fileService.findFileByIdAndKey(req.fileId, req.skey).get()
val result: MutableMap<String, Any> = HashMap()
if (file != null) {
file.apply {
filename = req.filename
description = req.description
}
fileService.saveFile(file)

result[Params.SUCCESS] = true
} else {
result[Params.SUCCESS] = false
}
return result
}

@GetMapping("/delete/{fileId}/{skey}/{filename}")
fun delete(@PathVariable fileId: Long, @PathVariable skey: String, @PathVariable filename: String) {
fileService.deleteFile(fileId, skey, filename)
}
}

+ 45
- 0
src/main/java/com/ffii/tsms/modules/file/web/UploadFileController.kt View File

@@ -0,0 +1,45 @@
package com.ffii.tsms.modules.file.web

import com.ffii.core.utils.StringUtils
import com.ffii.tsms.modules.file.entity.File
import com.ffii.tsms.modules.file.service.FileService
import com.ffii.tsms.modules.settings.service.SettingsService
import jakarta.servlet.http.HttpServletRequest
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.ui.Model
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.io.IOException

@RestController
@RequestMapping(value = ["/file"])
class UploadFileController(private val settingsService: SettingsService, private val fileService: FileService) {
private val logger: Log = LogFactory.getLog(javaClass)

@PostMapping("/ul")
@Throws(ServletRequestBindingException::class, IOException::class)
fun uploadFile(
request: HttpServletRequest,
model: Model,
@RequestParam refId: Long,
@RequestParam refType: String,
@RequestParam(defaultValue = StringUtils.EMPTY) refCode: String,
@RequestParam multipartFileList: List<MultipartFile>
): Map<String, Any> {
var result: Map<String, Any> = HashMap<String, Any>()

if (multipartFileList != null) {
for (multipartFile in multipartFileList) {
result = fileService.uploadFile(multipartFile, refId, refType, refCode, File().DEFAULT_UPLOAD_MAX_FILE_SIZE_MB)
}
}

// only proceed if multipartFile is not null, and has file size
return result
}
}

+ 10
- 0
src/main/java/com/ffii/tsms/modules/file/web/model/FileReq.kt View File

@@ -0,0 +1,10 @@
package com.ffii.tsms.modules.file.web.model

data class FileReq(
val fileId: Long,
val fileName: String,
val refType: String,
val refId: String,
val refCode: String,
val skey: String,
)

+ 8
- 0
src/main/java/com/ffii/tsms/modules/file/web/model/UpdateFileReq.kt View File

@@ -0,0 +1,8 @@
package com.ffii.tsms.modules.file.web.model

data class UpdateFileReq (
val fileId: Long,
val filename: String,
val skey: String,
val description: String,
)

+ 11
- 0
src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java View File

@@ -134,6 +134,17 @@ public class SettingsService extends AbstractIdEntityService<Settings, Long, Set
.orElseThrow(InternalServerErrorException::new);
}

public int getInt(String name, int defaultValue) {
return this.findByName(name)
.map(Settings::getValue)
.map(v -> {
try {
return Integer.parseInt(v);
} catch (final NumberFormatException nfe) {
return defaultValue;
}
}).orElse(defaultValue);
}
public double getDouble(String name) {
return this.findByName(name)
.map(Settings::getValue)


+ 13
- 0
src/main/resources/db/changelog/changes/20240419_01_cyril/01_update_claim.sql View File

@@ -0,0 +1,13 @@
-- liquibase formatted sql
-- changeset cyril:update claim

ALTER TABLE `claim`
CHANGE COLUMN `decision` `type` VARCHAR(20) NOT NULL ,
ADD INDEX `FK_CLAIM_ON_VERIFIEDBY` (`verifiedBy` ASC) VISIBLE;
;
ALTER TABLE `claim`
ADD CONSTRAINT `FK_CLAIM_ON_VERIFIEDBY`
FOREIGN KEY (`verifiedBy`)
REFERENCES `staff` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION;

+ 50
- 0
src/main/resources/db/changelog/changes/20240419_01_cyril/02_update_claim.sql View File

@@ -0,0 +1,50 @@
-- liquibase formatted sql
-- changeset cyril:create claim_detail, update claim

CREATE TABLE `claim_detail` (
`id` INT NOT NULL AUTO_INCREMENT,
`version` INT NOT NULL DEFAULT '0',
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` VARCHAR(30) NULL DEFAULT NULL,
`modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` VARCHAR(30) NULL DEFAULT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT '0',
`claimId` INT NOT NULL,
`invoiceDate` DATETIME NOT NULL,
`projectId` INT NOT NULL,
`description` VARCHAR(255) NOT NULL,
`amount` DECIMAL(14,2) NOT NULL,
`fileId` INT NOT NULL,
PRIMARY KEY (`id`),
INDEX `FK_CLAIM_DETAIL_ON_CLAIMID` (`claimId` ASC) VISIBLE,
INDEX `FK_CLAIM_DETAIL_ON_PROJECTID_` (`projectId` ASC) VISIBLE,
INDEX `FK_CLAIM_DETAIL_ON_FILEID` (`fileId` ASC) VISIBLE,
CONSTRAINT `FK_CLAIM_DETAIL_ON_CLAIMID`
FOREIGN KEY (`claimId`)
REFERENCES `claim` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `FK_CLAIM_DETAIL_ON_PROJECTID`
FOREIGN KEY (`projectId`)
REFERENCES `project` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `FK_CLAIM_DETAIL_ON_FILEID`
FOREIGN KEY (`fileId`)
REFERENCES `file` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION);

ALTER TABLE `claim`
DROP FOREIGN KEY `FK_CLAIM_ON_PROJECTID`,
DROP FOREIGN KEY `FK_CLAIM_ON_FILEID`;
ALTER TABLE `claim`
DROP COLUMN `approvedAmount`,
DROP COLUMN `fileId`,
DROP COLUMN `amount`,
DROP COLUMN `description`,
DROP COLUMN `projectId`,
ADD COLUMN `status` VARCHAR(30) NOT NULL AFTER `type`,
DROP INDEX `FK_CLAIM_ON_FILEID` ,
DROP INDEX `FK_CLAIM_ON_PROJECTID` ;
;

+ 6
- 0
src/main/resources/db/changelog/changes/20240419_01_cyril/03_update_claim.sql View File

@@ -0,0 +1,6 @@
-- liquibase formatted sql
-- changeset cyril:update claim

ALTER TABLE `claim`
;
ALTER TABLE `claim` ALTER INDEX `FK_CLAIM_ON_STAFFID` VISIBLE;

+ 5
- 0
src/main/resources/db/changelog/changes/20240419_01_cyril/04_update_claim_detail.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:update claim

ALTER TABLE `claim_detail`
CHANGE COLUMN `invoiceDate` `invoiceDate` DATE NOT NULL ;

+ 5
- 0
src/main/resources/db/changelog/changes/20240424_01_cyril/01_update_claim.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:update claim

ALTER TABLE `claim`
ADD COLUMN `code` VARCHAR(30) NULL AFTER `deleted`;

Loading…
Cancel
Save