diff --git a/include/globals.h b/include/globals.h index 24dae02..85afab9 100644 --- a/include/globals.h +++ b/include/globals.h @@ -6,5 +6,6 @@ #include extern char productPricesFileLocation[64]; // Dichiarazione extern +extern char countFileLocation[64]; #endif diff --git a/include/logic.h b/include/logic.h index 1e29679..b916721 100644 --- a/include/logic.h +++ b/include/logic.h @@ -1,19 +1,36 @@ #ifndef LOGIC_H #define LOGIC_H + #include #include #include #include +#include struct product { - float cost; - int numberSold; - char name; + float cost; + int numberSold; + char name; }; -int getCount(); +/* File */ int existsFile(const char *fname); -void createFile(const char *fname); +int createFile(const char *fname); +int renameDataFile(const char *oldName, const char *newName); + +/* Prodotti */ +json_object *getProducts(const char *fname); +void listProducts(const char *fname); +int addProduct(const char *fname, const char *name, double price); +double getPrice(const char *fname, const char *name); + +/* Copie vendute */ +int getCount(const char *fname, const char *name); +void listSold(const char *fname); +int addSold(const char *fname, const char *name, int qty); + +/* Entrate */ +double getTotalRevenue(const char *productsFile, const char *soldFile); #endif diff --git a/include/windowsDeclaration.h b/include/windowsDeclaration.h index 08961bd..f1e4ec6 100644 --- a/include/windowsDeclaration.h +++ b/include/windowsDeclaration.h @@ -8,6 +8,11 @@ int runMainMenu(); void runOptionsMenu(); +void runSellWindow(); +void runAboutWindow(); +void runViewSalesWindow(); +void runViewProductsWindow(); +void runAddProductWindow(); void example_window(); #endif diff --git a/makefile b/makefile index 5dccc0a..b9b10c9 100644 --- a/makefile +++ b/makefile @@ -1,28 +1,39 @@ -CC = gcc -CFLAGS = -I./include -Wall -Wextra -MMD -MP -LDLIBS = -lncurses +CC ?= gcc +PKG_CONFIG ?= pkg-config + +CFLAGS = -I./include -Wall -Wextra -MMD -MP $(shell $(PKG_CONFIG) --cflags json-c ncurses) +LDLIBS = $(shell $(PKG_CONFIG) --libs json-c ncurses) + +BUNDLE_LDFLAGS = -Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2 -Wl,-rpath,'$$ORIGIN/lib' + SRC_DIR = src BUILD_DIR = build SOURCES = $(filter-out $(SRC_DIR)/testLogic.c, $(wildcard $(SRC_DIR)/*.c)) OBJECTS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES)) -TEST_SOURCES = $(SRC_DIR)/logic.c $(SRC_DIR)/testLogic.c -TEST_OBJECTS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(TEST_SOURCES)) EXEC = productManager -EXECTEST = test DEPS = $(OBJECTS:.o=.d) all: $(EXEC) + +# 1. Per testare in locale su NixOS $(EXEC): $(OBJECTS) $(CC) $^ -o $@ $(LDLIBS) + $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @mkdir -p $(@D) $(CC) $(CFLAGS) -c $< -o $@ + -include $(DEPS) -test: $(TEST_OBJECTS) - $(CC) $^ -o $(EXECTEST) $(LDLIBS) +# 2. Per creare il pacchetto portabile da inviare +bundle: $(OBJECTS) + @mkdir -p dist/lib + $(CC) $^ -o dist/$(EXEC) $(BUNDLE_LDFLAGS) $(LDLIBS) + @cp -L $$( $(PKG_CONFIG) --variable=libdir json-c )/libjson-c.so* dist/lib/ + @cp -L $$( $(PKG_CONFIG) --variable=libdir ncurses )/libncurses*.so* dist/lib/ + @echo "=== Pacchetto portabile creato con successo in ./dist ===" clean: - rm -rf $(BUILD_DIR) $(EXEC) $(EXECTEST) + rm -rf $(BUILD_DIR) $(EXEC) dist *.json -.PHONY: all clean +.PHONY: all clean bundle diff --git a/shell.nix b/shell.nix index a99cc9a..e59a445 100644 --- a/shell.nix +++ b/shell.nix @@ -1,8 +1,12 @@ { pkgs ? import {} }: pkgs.mkShell { - buildInputs = [ - pkgs.gcc - pkgs.gnumake - pkgs.ncurses - ]; + nativeBuildInputs = [ + pkgs.gcc + pkgs.gnumake + pkgs.pkgconf + ]; + buildInputs = [ + pkgs.ncurses + pkgs.json_c + ]; } diff --git a/src/globals.c b/src/globals.c index 5aa5bde..f0130d9 100644 --- a/src/globals.c +++ b/src/globals.c @@ -1,4 +1,5 @@ #include "../include/globals.h" -char productPricesFileLocation[64]="./productPricesSettings.fpm"; -char countFileLocation[64]="./count.fpm"; +char productPricesFileLocation[64]="./pm-item-price-default.json"; +char countFileLocation[64]="./pm-item-sold-count-default.json"; + diff --git a/src/logic.c b/src/logic.c index 089ec05..2fc1eda 100644 --- a/src/logic.c +++ b/src/logic.c @@ -2,64 +2,300 @@ #include #include #include +#include #include "../include/logic.h" #include "../include/globals.h" /* -* TODO: Listare prodotti con prezzo da file -* TODO: Aggiungere prodotto con prezzo a file -* TODO: Listare copie vendute di prodotto da file -* TODO: Aggiungere copie venduto di prodotto a file -* TODO: Cambiare nome dei file con interagisce -* TODO: Calcolare la somma delle entrate dati i file -* TODO: Aggiungere la possibilità di aggiungere gli acquisti non compiuti con intenzioni di acquisto. -* e.g. persona vuole acquistare maglietta S ma non c'è la taglia -*/ + * Formato dei file (JSON): + * + * File prodotti: [ {"name": "Maglietta S", "price": 19.90}, ... ] + * File vendite: [ {"name": "Maglietta S", "sold": 12}, ... ] + * + * Compilazione: aggiungere -ljson-c (e -lncurses) al linker. + * + * TODO: Aggiungere la possibilità di registrare gli acquisti non compiuti + * con intenzioni di acquisto. + * e.g. persona vuole acquistare maglietta S ma non c'è la taglia + */ -char *products; +/* ------------------------------------------------------------------ */ +/* Utilità sui file */ +/* ------------------------------------------------------------------ */ int existsFile(const char *fname) { - FILE *file; - if ((file = fopen(fname, "r"))) - { + FILE *file = fopen(fname, "r"); + if (file) { fclose(file); return 1; } return 0; } -void createFile(const char *fname) { - FILE *file; - file = fopen(fname, "w"); - fclose(file); +/* Crea un file JSON contenente un array vuoto. Ritorna 0 se ok, -1 se errore. */ +int createFile(const char *fname) { + json_object *arr = json_object_new_array(); + if (!arr) + return -1; + + int res = json_object_to_file_ext(fname, arr, JSON_C_TO_STRING_PRETTY); + json_object_put(arr); + return (res == 0) ? 0 : -1; } -char* getProducts(const char *fname) { -/* FILE *file; - - if((file=fopen(fname, "r"))==NULL) { - printf("Errore nell'apertura del file"); - exit(1); - } - -// TODO QUESTO CODICE NON E' MIO, RICONTROLLARE FUNZIONAMENTO fread() - - while(!feof(file)){ - n = fread(buffer, sizeof(char), sizeof(buffer), file); - for (i=0; i -1.0"); + + json_object *arr = getProducts(PRODUCTS_FILE); + CHECK(arr != NULL && json_object_array_length(arr) == 2, "getProducts ritorna 2 prodotti"); + if (arr) json_object_put(arr); + + printf("== Copie vendute ==\n"); + CHECK(getCount(SOLD_FILE, "Maglietta S") == 0, "nessuna vendita all'inizio"); + CHECK(addSold(SOLD_FILE, "Maglietta S", 2) == 0, "addSold Maglietta S +2"); + CHECK(addSold(SOLD_FILE, "Maglietta S", 1) == 0, "addSold Maglietta S +1"); + CHECK(addSold(SOLD_FILE, "Pantaloni", 2) == 0, "addSold Pantaloni +2"); + CHECK(getCount(SOLD_FILE, "Maglietta S") == 3, "Maglietta S: 3 copie"); + CHECK(getCount(SOLD_FILE, "Pantaloni") == 2, "Pantaloni: 2 copie"); + CHECK(getCount(SOLD_FILE, "Inesistente") == 0, "prodotto mai venduto: 0 copie"); + + printf("== Entrate ==\n"); + /* 3 * 20.00 + 2 * 35.50 = 131.00 */ + CHECK(near(getTotalRevenue(PRODUCTS_FILE, SOLD_FILE), 131.00), "entrate totali = 131.00"); + + /* Una vendita di un prodotto non presente nel listino non deve contare */ + addSold(SOLD_FILE, "Fantasma", 10); + CHECK(near(getTotalRevenue(PRODUCTS_FILE, SOLD_FILE), 131.00), "vendite senza prezzo ignorate"); + + printf("== Rinomina ==\n"); + CHECK(renameDataFile(PRODUCTS_FILE, RENAMED_FILE) == 0, "renameDataFile ritorna 0"); + CHECK(existsFile(PRODUCTS_FILE) == 0 && existsFile(RENAMED_FILE) == 1, "vecchio nome sparito, nuovo presente"); + CHECK(near(getPrice(RENAMED_FILE, "Pantaloni"), 35.50), "i dati sopravvivono alla rinomina"); + CHECK(renameDataFile("non_esiste.fpm", "altro.fpm") == -1, "rinomina di file inesistente fallisce"); + + cleanup(); + + printf("\nRisultato: %d ok, %d falliti\n", passed, failed); + return failed == 0 ? 0 : 1; } diff --git a/src/windowsDeclaration.c b/src/windowsDeclaration.c index 5802349..971b657 100644 --- a/src/windowsDeclaration.c +++ b/src/windowsDeclaration.c @@ -8,6 +8,147 @@ #include "../include/logic.h" #include "../include/globals.h" #define MAX_LEN 64 +#define LINE_LEN 96 + +/* + * + * HELPER + * + * + */ + + +static void freeLines(char **lines, int n) { + for (int i = 0; i < n; i++) + free(lines[i]); + free(lines); +} + +// Legge un campo stringa da un elemento JSON ("?" se manca) +static const char *jsonStr(json_object *item, const char *key) { + json_object *v = NULL; + const char *s = NULL; + if (json_object_object_get_ex(item, key, &v) && v) + s = json_object_get_string(v); + return s ? s : "?"; +} + +static double jsonDouble(json_object *item, const char *key) { + json_object *v = NULL; + return (json_object_object_get_ex(item, key, &v) && v) ? json_object_get_double(v) : 0.0; +} + +static int jsonInt(json_object *item, const char *key) { + json_object *v = NULL; + return (json_object_object_get_ex(item, key, &v) && v) ? json_object_get_int(v) : 0; +} + +// Costruisce le righe "nome prezzo" dall'array dei prodotti. +// *n contiene il numero di righe create. Ritorna NULL se malloc fallisce. +static char **buildProductLines(json_object *products, int *n) { + int count = (int)json_object_array_length(products); + char **lines = malloc((count > 0 ? count : 1) * sizeof *lines); + if (!lines) { + *n = 0; + return NULL; + } + + int made = 0; + for (int i = 0; i < count; i++) { + json_object *item = json_object_array_get_idx(products, i); + lines[i] = malloc(LINE_LEN); + if (!lines[i]) + break; + snprintf(lines[i], LINE_LEN, "%-26.26s %8.2f", + jsonStr(item, "name"), jsonDouble(item, "price")); + made++; + } + *n = made; + return lines; +} + +/* + * Finestra a elenco scorrevole. + * - selectable = 1: ritorna l'indice scelto con INVIO/SPAZIO, -1 se annullato (q) + * - selectable = 0: sola lettura, ritorna sempre -1 + * "footer" (puo' essere NULL) e' una riga di testo fissa sopra i comandi. + */ +static int listWindow(const char *title, char **lines, int n, + const char *footer, int selectable) { + int height = LINES - 4; + int width = COLS / 2; + int visible = height - 6; + int textw = width - 4; + if (visible < 1) visible = 1; + if (textw < 0) textw = 0; + + WINDOW *win = create_newwin(height, width, 0, COLS / 2); + keypad(win, TRUE); + mvwprintw(win, 1, 2, "%.*s", textw, title); + + int sel = 0, top = 0, result = -1, done = 0; + + while (!done) { + // Mantiene la selezione dentro l'area visibile + if (sel < top) top = sel; + if (sel >= top + visible) top = sel - visible + 1; + + for (int i = 0; i < visible; i++) { + mvwhline(win, 3 + i, 1, ' ', width - 2); + int idx = top + i; + if (idx >= n) continue; + if (selectable && idx == sel) wattron(win, A_REVERSE); + mvwprintw(win, 3 + i, 2, "%.*s", textw, lines[idx]); + if (selectable && idx == sel) wattroff(win, A_REVERSE); + } + if (n == 0) + mvwprintw(win, 3, 2, "Nessun elemento."); + + mvwhline(win, height - 3, 1, ' ', width - 2); + if (footer) + mvwprintw(win, height - 3, 2, "%.*s", textw, footer); + + mvwhline(win, height - 2, 1, ' ', width - 2); + mvwprintw(win, height - 2, 2, "%.*s", textw, + selectable ? "SU/GIU scorri INVIO scegli q annulla" + : "SU/GIU scorri q chiudi"); + wrefresh(win); + + switch (wgetch(win)) { + case KEY_UP: case 'k': if (sel > 0) sel--; break; + case KEY_DOWN: case 'j': if (sel < n - 1) sel++; break; + case KEY_PPAGE: + sel -= visible; + if (sel < 0) sel = 0; + break; + case KEY_NPAGE: + sel += visible; + if (sel > n - 1) sel = n - 1; + if (sel < 0) sel = 0; + break; + case 10: case 32: + if (selectable && n > 0) result = sel; + done = 1; + break; + case 'q': + done = 1; + break; + } + } + + werase(win); + destroy_win(win); + return result; +} + + + +/* + * + * Finestre + * + * + */ void example_window() { WINDOW *win; @@ -41,8 +182,8 @@ int runMainMenu(){ "Segna vendita", "Vedi vendite", "Vedi prodotti", - "Gestisci prodotti", - "Opzioni", + "Aggiungi prodotti", + "About", "Exit" }; int n_mainMenu = sizeof(mainMenu) / sizeof(char *); @@ -67,6 +208,63 @@ int runMainMenu(){ return choice; } +void runAboutWindow(){ + WINDOW *aboutWin; + aboutWin = create_newwin(LINES/2-2, COLS/2, LINES/2, 0); + + wwrap_print(aboutWin, 1, 1, "Questo e' un software fatto per Roma2LUG, il codice verra' pubblicato sul gitea del lug in futuro. La parte logica e' fatta con Claude Code, non avevo voglia di scrivere quei pezzi e volevo effettivamente rendere utile il codice che avevo scritto per gestire ncurses. Oh well, spero vi possa essere comunque utile. Buon lavoro / Divertimento!"); + statusf("Uovo di pasqua ()"); + wrefresh(aboutWin); + getch(); + werase(aboutWin); + destroy_win(aboutWin); +} + +void runAddProductWindow() { + WINDOW *addWin = create_newwin(LINES / 2, COLS / 2, 0, COLS / 2); + + char name[33] = ""; + char priceStr[13] = ""; + char *opts[] = {"Salva", "Annulla"}; + + mvwprintw(addWin, 1, 2, "Aggiungi un nuovo prodotto"); + + mvwprintw(addWin, 3, 2, "Nome:"); + uiTextInput(addWin, 4, 2, name, 32); + + mvwprintw(addWin, 6, 2, "Prezzo (es. 19.90):"); + uiTextInput(addWin, 7, 2, priceStr, 12); + + int choice = uiSelectMenu(addWin, 9, 4, opts, 2); + + if (choice == 1) { + for (char *p = priceStr; *p; p++) + if (*p == ',') *p = '.'; + + char *end; + double price = strtod(priceStr, &end); + + if (name[0] == '\0') { + warningf("Il nome del prodotto non puo' essere vuoto."); + } else if (priceStr[0] == '\0' || *end != '\0' || price < 0) { + warningf("Il prezzo '%s' non e' valido.", priceStr); + } else { + int res = addProduct(productPricesFileLocation, name, price); + if (res == 0) + statusf("Prodotto '%s' aggiunto a %.2f", name, price); + else if (res == 1) + warningf("Il prodotto '%s' esiste gia'.", name); + else + warningf("Errore nella scrittura del file '%s'.", productPricesFileLocation); + } + } else { + statusf("Operazione annullata"); + } + + werase(addWin); + destroy_win(addWin); +} + void runOptionsMenu(){ WINDOW *optionsWin; optionsWin = create_newwin(LINES/2, COLS/2, 0, COLS/2); @@ -87,3 +285,157 @@ void runOptionsMenu(){ werase(optionsWin); destroy_win(optionsWin); } + +/* ------------------------------------------------------------------ */ +/* Segna vendita */ +/* ------------------------------------------------------------------ */ + +void runSellWindow() { + json_object *products = getProducts(productPricesFileLocation); + if (!products) { + warningf("Impossibile leggere il file '%s'.", productPricesFileLocation); + return; + } + + if (json_object_array_length(products) == 0) { + warningf("Non ci sono prodotti. Aggiungine uno da 'Gestisci prodotti'."); + json_object_put(products); + return; + } + + // 1. Scelta del prodotto + int n; + char **lines = buildProductLines(products, &n); + if (!lines) { + warningf("Memoria insufficiente."); + json_object_put(products); + return; + } + + int idx = listWindow("Segna vendita - scegli il prodotto", lines, n, NULL, 1); + freeLines(lines, n); + + if (idx < 0) { + statusf("Operazione annullata"); + json_object_put(products); + return; + } + + json_object *item = json_object_array_get_idx(products, idx); + const char *name = jsonStr(item, "name"); + double price = jsonDouble(item, "price"); + + // 2. Quantita' venduta + WINDOW *win = create_newwin(9, COLS / 2, 0, COLS / 2); + keypad(win, TRUE); + + char qtyStr[8] = "1"; + char *opts[] = {"Salva", "Annulla"}; + + mvwprintw(win, 1, 2, "Segna vendita"); + mvwprintw(win, 3, 2, "Prodotto: %.30s", name); + mvwprintw(win, 4, 2, "Quantita' venduta:"); + uiTextInput(win, 5, 2, qtyStr, 6); + + int choice = uiSelectMenu(win, 6, 4, opts, 2); + + if (choice == 1) { + char *end; + long qty = strtol(qtyStr, &end, 10); + + if (qtyStr[0] == '\0' || *end != '\0' || qty <= 0 || qty > 1000000) { + warningf("La quantita' '%s' non e' valida.", qtyStr); + } else if (addSold(countFileLocation, name, (int)qty) == 0) { + statusf("Registrata vendita: %ld x '%s' = %.2f", qty, name, qty * price); + } else { + warningf("Errore nella scrittura del file '%s'.", countFileLocation); + } + } else { + statusf("Operazione annullata"); + } + + werase(win); + destroy_win(win); + json_object_put(products); +} + +/* ------------------------------------------------------------------ */ +/* Vedi vendite */ +/* ------------------------------------------------------------------ */ + +void runViewSalesWindow() { + json_object *sales = getProducts(countFileLocation); + if (!sales) { + warningf("Impossibile leggere il file '%s'.", countFileLocation); + return; + } + + int count = (int)json_object_array_length(sales); + char **lines = malloc((count > 0 ? count : 1) * sizeof *lines); + if (!lines) { + warningf("Memoria insufficiente."); + json_object_put(sales); + return; + } + + int n = 0; + for (int i = 0; i < count; i++) { + json_object *item = json_object_array_get_idx(sales, i); + const char *name = jsonStr(item, "name"); + int sold = jsonInt(item, "sold"); + double price = getPrice(productPricesFileLocation, name); + + lines[i] = malloc(LINE_LEN); + if (!lines[i]) + break; + + if (price >= 0) + snprintf(lines[i], LINE_LEN, "%-18.18s %4d %9.2f", name, sold, sold * price); + else + snprintf(lines[i], LINE_LEN, "%-18.18s %4d %9s", name, sold, "n/d"); + n++; + } + + char footer[64]; + double total = getTotalRevenue(productPricesFileLocation, countFileLocation); + if (total >= 0) + snprintf(footer, sizeof footer, "Entrate totali: %.2f", total); + else + snprintf(footer, sizeof footer, "Entrate totali: n/d"); + + listWindow("Vendite (prodotto / copie / incasso)", lines, n, footer, 0); + + freeLines(lines, n); + json_object_put(sales); +} + +/* ------------------------------------------------------------------ */ +/* Vedi prodotti */ +/* ------------------------------------------------------------------ */ + +void runViewProductsWindow() { + json_object *products = getProducts(productPricesFileLocation); + if (!products) { + warningf("Impossibile leggere il file '%s'.", productPricesFileLocation); + return; + } + + int n; + char **lines = buildProductLines(products, &n); + if (!lines) { + warningf("Memoria insufficiente."); + json_object_put(products); + return; + } + + char footer[64]; + snprintf(footer, sizeof footer, "Prodotti in listino: %d", n); + + listWindow("Prodotti (nome / prezzo)", lines, n, footer, 0); + + freeLines(lines, n); + json_object_put(products); +} + + +